Skip to content
Closed
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
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
required: false
default: ""

# Auth configuration
anthropic_api_key:
Expand Down Expand Up @@ -208,6 +212,7 @@ runs:
CLAUDE_CODE_ACTION: "1"
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
Expand Down
5 changes: 5 additions & 0 deletions base-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
required: false
default: ""

# Action settings
claude_args:
Expand Down Expand Up @@ -123,6 +127,7 @@ runs:
INPUT_PROMPT: ${{ inputs.prompt }}
INPUT_PROMPT_FILE: ${{ inputs.prompt_file }}
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
Expand Down
7 changes: 7 additions & 0 deletions base-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env";
import { installPlugins } from "./install-plugins";

async function run() {
try {
Expand All @@ -15,6 +16,12 @@ async function run() {
undefined, // homeDir
);

// Install plugins if specified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SECURITY: Missing Validation for Claude Executable Path

The INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE environment variable is passed directly to spawn() without validation, creating a command injection vulnerability.

An attacker who can control this input could execute arbitrary commands:

path_to_claude_code_executable: "/bin/bash -c 'malicious command' #"

Recommendation: Add validation before calling installPlugins():

const claudeExecutable = process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE || "claude";

// Validate executable path
if (claudeExecutable !== "claude") {
  if (!claudeExecutable.startsWith('/') || /[;&|`$()<>]/.test(claudeExecutable)) {
    throw new Error(`Invalid claude executable path: ${claudeExecutable}`);
  }
}

await installPlugins(process.env.INPUT_PLUGINS, claudeExecutable);

await installPlugins(
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE || "claude",
);

const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
promptFile: process.env.INPUT_PROMPT_FILE || "",
Expand Down
80 changes: 80 additions & 0 deletions base-action/src/install-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bun

import { spawn } from "child_process";

// Declare console as global for TypeScript
declare const console: {
log: (message: string) => void;
error: (message: string) => void;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unnecessary console type declaration

This custom console type declaration is inconsistent with the rest of the codebase. Other files like setup-claude-code-settings.ts and run-claude.ts use console without custom declarations, and the project uses @actions/core for structured logging.

Recommendation: Remove this declaration entirely and use @actions/core for consistency:

import * as core from "@actions/core";

// Replace console.log with core.info
// Replace console.error with core.error

This provides better GitHub Actions integration and log formatting.

Comment on lines +5 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: This console declaration should be removed.

Issues:

  1. console is a global object in Node.js/Bun - this declaration is unnecessary
  2. The type definition is incomplete (missing warn, info, debug, etc.)
  3. This pattern doesn't exist anywhere else in the codebase
  4. TypeScript already knows about console from built-in type definitions

Recommendation: Delete lines 5-9 entirely.


/**
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOCUMENTATION: Missing JSDoc comment for this exported function. All other exported functions have JSDoc comments.

Suggested change
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
/**
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
* @param pluginsInput - Comma-separated string of plugin names or undefined
* @returns Array of trimmed, non-empty plugin names
*/

*/
export function parsePlugins(pluginsInput: string | undefined): string[] {
if (!pluginsInput || pluginsInput.trim() === "") {
return [];
}

return pluginsInput
.split(",")
.map((plugin) => plugin.trim())
.filter((plugin) => plugin.length > 0);
}

/**
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL Security Vulnerability: The claudeExecutable parameter has no validation, allowing arbitrary command execution.

An attacker controlling path_to_claude_code_executable input could execute any binary:

path_to_claude_code_executable: "/usr/bin/curl"  # or any malicious script

Required Fix: Add validation before line 50:

if (!isValidClaudeExecutable(claudeExecutable)) {
  throw new Error(
    `Invalid Claude executable path: '${claudeExecutable}'. Must be 'claude' or an absolute path to a claude binary in a safe directory.`,
  );
}

And add this validation function:

function isValidClaudeExecutable(executablePath: string): boolean {
  if (executablePath === "claude") return true;
  
  if (path.isAbsolute(executablePath)) {
    const normalized = path.normalize(executablePath);
    if (normalized.includes("..")) return false;
    
    // Reject writable system directories
    const dangerousDirs = ["/tmp/", "/var/tmp/", "/dev/"];
    if (dangerousDirs.some(dir => normalized.startsWith(dir))) return false;
    
    // Ensure filename contains "claude"
    if (!path.basename(normalized).startsWith("claude")) return false;
    
    return true;
  }
  
  return false;
}

* Installs a single Claude Code plugin
*/
export async function installPlugin(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add JSDoc documentation

This function is missing complete JSDoc documentation including @param, @returns, @throws, and @example tags.

Recommendation:

/**
 * Installs a single Claude Code plugin by executing the Claude CLI plugin install command.
 * 
 * @param pluginName - The name of the plugin to install (e.g., "feature-dev", "@scope/plugin-name")
 * @param claudeExecutable - Path to the Claude Code executable (defaults to "claude")
 * @returns A Promise that resolves when installation succeeds
 * @throws {Error} When plugin installation fails or the Claude executable is not found
 * 
 * @example
 * ```typescript
 * await installPlugin("feature-dev");
 * await installPlugin("@myorg/custom-plugin", "/usr/local/bin/claude");
 * ```
 */

Similar documentation should be added to installPlugins() as well.

pluginName: string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium Security Issue: claudeExecutable parameter not validated

The claudeExecutable parameter is passed directly to spawn() without validation. While GitHub Actions provides input sanitization, defense-in-depth requires validating this parameter.

Recommendation: Add validation:

function isValidExecutablePath(path: string): boolean {
  const allowedNames = ['claude', 'claude-code'];
  const basename = path.split('/').pop() || '';
  
  if (!path.includes('/')) {
    return allowedNames.includes(path);
  }
  
  return (path.startsWith('/usr/local/bin/') || 
          path.startsWith('/home/')) &&
         allowedNames.includes(basename);
}

Then validate before spawning:

if (!isValidExecutablePath(claudeExecutable)) {
  throw new Error(`Invalid executable path: '${claudeExecutable}'`);
}

Reference: CWE-78 (OS Command Injection)

claudeExecutable: string = "claude",
): Promise<void> {
return new Promise((resolve, reject) => {
const process = spawn(claudeExecutable, ["plugin", "install", pluginName], {
stdio: "inherit",
});

process.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else {
reject(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Duplicate error handler with broken structure

Lines 78-85 contain a duplicate error event handler with malformed structure. There's an empty handler on line 78, then the actual handler is nested inside it on lines 80-84.

This should be:

  process.on("close", (code: number | null) => {
    if (code === 0) {
      resolve();
    } else {
      reject(
        new Error(
          `Failed to install plugin '${pluginName}' (exit code: ${code ?? 'unknown'})`,
        ),
      );
    }
  });

  process.on("error", (err: Error) => {
    reject(
      new Error(`Failed to install plugin '${pluginName}': ${err.message}`),
    );
  });
}

new Error(
`Failed to install plugin '${pluginName}' (exit code: ${code})`,
),
);
Comment on lines +37 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality: Exit code handling could be more explicit about null cases.

While this works in practice, making the null handling explicit improves clarity:

process.on("close", (code: number | null) => {
  if (code === 0) {
    resolve();
  } else {
    reject(
      new Error(
        `Failed to install plugin '${pluginName}' (exit code: ${code ?? "unknown"})`,
      ),
    );
  }
});

This makes it clear that code can be null and provides a better error message in that case.

}
});

process.on("error", (err: Error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error messages for spawn failures

When spawn fails, the error usually indicates the executable wasn't found or couldn't be started. The current error message doesn't distinguish between different failure types.

Recommendation: Enhance error messages to guide users:

process.on("error", (err: Error) => {
  if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
    reject(
      new Error(
        `Failed to install plugin '${pluginName}': Claude executable '${claudeExecutable}' not found. ` +
        `Ensure Claude Code is installed and the path is correct.`
      )
    );
  } else {
    reject(
      new Error(`Failed to spawn plugin install process for '${pluginName}': ${err.message}`)
    );
  }
});

reject(
new Error(`Failed to install plugin '${pluginName}': ${err.message}`),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation Issue: JSDoc doesn't mention parallel execution

The implementation uses Promise.allSettled() on line 104 which installs plugins in parallel, not sequentially. Update the JSDoc:

Suggested change
);
/**
* Installs Claude Code plugins from a comma-separated list in parallel
*/

});
});
}

/**
* Installs Claude Code plugins from a comma-separated list
*/
export async function installPlugins(
Comment on lines +55 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Syntax Error: Multiple issues here:

  1. Missing closing brace after return on line 94
  2. Duplicate console.log statement (appears on both line 95 and 97)
Suggested change
}
/**
* Installs Claude Code plugins from a comma-separated list
*/
export async function installPlugins(
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
console.log(`Installing ${plugins.length} plugin(s) in parallel...`);

pluginsInput: string | undefined,
claudeExecutable: string = "claude",
): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Broken control flow and duplicate console.log

The return statement on line 101 is missing its closing brace, and the console.log is duplicated on lines 102 and 104.

This should be:

  if (plugins.length === 0) {
    console.log("No plugins to install");
    return;
  }

  console.log(`Installing ${plugins.length} plugin(s) in parallel...`);

const plugins = parsePlugins(pluginsInput);

Comment on lines +60 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Syntax error - missing closing brace and duplicate console.log

There's a missing } after the return statement on line 99, and the console.log on line 102 is duplicated from line 100.

Suggested change
export async function installPlugins(
pluginsInput: string | undefined,
claudeExecutable: string = "claude",
): Promise<void> {
const plugins = parsePlugins(pluginsInput);
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
console.log(`Installing ${plugins.length} plugin(s) in parallel...`);

if (plugins.length === 0) {
Comment on lines +62 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Duplicate console.log and missing return

Lines 100 and 102 both log "Installing plugins in parallel". Remove the duplicate:

Suggested change
claudeExecutable: string = "claude",
): Promise<void> {
const plugins = parsePlugins(pluginsInput);
if (plugins.length === 0) {
}
console.log(`Installing ${plugins.length} plugin(s) in parallel...`);
const results = await Promise.allSettled(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate console.log statement

This line is a duplicate of line 101. Remove this redundant logging.

Suggested change
if (plugins.length === 0) {

console.log("No plugins to install");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate console.log statement

Two nearly identical log messages on lines 102 and 104. Remove one or make them meaningfully different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Early Return Makes All Plugin Installation Code Unreachable

This early return statement causes lines 105-147 (the entire plugin installation logic) to be unreachable. This makes the plugin installation feature completely non-functional.

The lines below also contain multiple issues:

  • Lines 107-109: Same console.log repeated 4 times
  • Lines 126-143: Duplicate nested error handling
  • Missing proper Promise.allSettled result handling
Suggested change
console.log("No plugins to install");
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
console.log(`Installing ${plugins.length} plugin(s) in parallel...`);
const results = await Promise.allSettled(
plugins.map(async (plugin) => {
try {
await installPlugin(plugin, claudeExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
return { plugin, status: 'success' as const };
} catch (error) {
console.error(`✗ Failed to install: ${plugin}`);
return { plugin, status: 'failed' as const, error };
}
})
);
const failed = results.filter(
(r) => r.status === "fulfilled" && r.value.status === "failed"
);
if (failed.length > 0) {
const failureDetails = failed
.map(r => {
const value = (r as PromiseFulfilledResult<any>).value;
return `${value.plugin}: ${value.error?.message || 'unknown error'}`;
})
.join('; ');
throw new Error(
`Failed to install ${failed.length} plugin(s): ${failureDetails}`
);
}
console.log("All plugins installed successfully");
}

return;
}

console.log(`Installing ${plugins.length} plugin(s)...`);

for (const plugin of plugins) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider parallel plugin installation

Plugins are currently installed sequentially. For users specifying multiple plugins, this could add 10-30+ seconds to action execution time.

Recommendation: Install plugins in parallel using Promise.all():

console.log(`Installing ${plugins.length} plugin(s) in parallel...`);

await Promise.all(
  plugins.map(async (plugin) => {
    console.log(`Installing plugin: ${plugin}`);
    await installPlugin(plugin, claudeExecutable);
    console.log(`✓ Successfully installed: ${plugin}`);
  })
);

Impact: 60-70% reduction in installation time for multiple plugins.

console.log(`Installing plugin: ${plugin}`);
await installPlugin(plugin, claudeExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
}
Comment on lines +73 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance & Error Handling: Sequential installation is inefficient and provides poor error feedback.

Issues:

  1. Installing plugins one-by-one adds significant CI/CD time (e.g., 5 plugins × 10s = 50s total vs 10s in parallel)
  2. If plugin 3 fails, users won't know plugins 1-2 succeeded
  3. Plugins 4-5 never get attempted

Recommendation: Use Promise.allSettled() for parallel installation with better error reporting:

const results = await Promise.allSettled(
  plugins.map(async (plugin) => {
    console.log(`Installing plugin: ${plugin}`);
    await installPlugin(plugin, claudeExecutable);
    console.log(`✓ Successfully installed: ${plugin}`);
  })
);

const failures = results
  .map((result, idx) => ({ result, plugin: plugins[idx] }))
  .filter(({ result }) => result.status === "rejected");

if (failures.length > 0) {
  failures.forEach(({ plugin, result }) => {
    console.error(`✗ Failed: ${plugin} - ${result.reason}`);
  });
  throw new Error(
    `Failed to install ${failures.length}/${plugins.length} plugin(s): ${failures.map(f => f.plugin).join(", ")}`
  );
}

This provides 60-80% faster installation and clear failure reporting.


console.log("All plugins installed successfully");
}
84 changes: 84 additions & 0 deletions base-action/test/install-plugins.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bun

import { describe, test, expect } from "bun:test";
import { parsePlugins } from "../src/install-plugins";

describe("parsePlugins", () => {
test("should return empty array for undefined input", () => {
expect(parsePlugins(undefined)).toEqual([]);
});

test("should return empty array for empty string", () => {
expect(parsePlugins("")).toEqual([]);
});

test("should return empty array for whitespace-only string", () => {
expect(parsePlugins(" \n\t ")).toEqual([]);
});

test("should parse single plugin", () => {
expect(parsePlugins("feature-dev")).toEqual(["feature-dev"]);
});

test("should parse multiple plugins", () => {
expect(parsePlugins("feature-dev,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});

test("should trim whitespace around plugin names", () => {
expect(parsePlugins(" feature-dev , test-coverage-reviewer ")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});

test("should handle spaces between commas", () => {
expect(
parsePlugins(
"feature-dev, test-coverage-reviewer, code-quality-reviewer",
),
).toEqual([
"feature-dev",
"test-coverage-reviewer",
"code-quality-reviewer",
]);
});

test("should filter out empty values from consecutive commas", () => {
expect(parsePlugins("feature-dev,,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});

test("should handle trailing comma", () => {
expect(parsePlugins("feature-dev,test-coverage-reviewer,")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});

test("should handle leading comma", () => {
expect(parsePlugins(",feature-dev,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});

test("should handle plugins with special characters", () => {
expect(parsePlugins("@scope/plugin-name,plugin-name-2")).toEqual([
"@scope/plugin-name",
"plugin-name-2",
]);
});

test("should handle complex whitespace patterns", () => {
expect(
parsePlugins(
"\n feature-dev \n,\t test-coverage-reviewer\t, code-quality \n",
),
).toEqual(["feature-dev", "test-coverage-reviewer", "code-quality"]);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Missing test coverage for core functionality

Current tests only cover parsePlugins() (11 tests), but provide zero coverage for:

  • installPlugin() - subprocess spawning, error handling, process failures
  • installPlugins() - sequential execution, error propagation
  • Integration with index.ts

High-risk untested scenarios:

  • Process exits with non-zero code
  • Process spawn fails (ENOENT)
  • Null exit codes
  • Multiple plugin failures
  • Security: malicious plugin names (--version, ../../etc/passwd)

Recommend adding tests with mocked spawn():

import { spawn } from "child_process";
import { mock } from "bun:test";

describe("installPlugin", () => {
  test("should reject when process exits with non-zero code", async () => {
    // Mock spawn to emit exit code 1
    await expect(installPlugin("bad-plugin")).rejects.toThrow(
      "Failed to install plugin 'bad-plugin' (exit code: 1)"
    );
  });
  
  test("should reject malicious plugin names", async () => {
    await expect(installPlugin("--version")).rejects.toThrow("Invalid plugin name");
  });
});

Without these tests, production failures are likely to go undetected until runtime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Missing Test Coverage

The test file only covers parsePlugins() (11 tests), but the two most critical functions have zero test coverage:

  • installPlugin() - spawns child processes, handles exit codes and errors
  • installPlugins() - orchestrates parallel installation, aggregates errors

This creates significant risk of production failures. Essential missing tests:

// Mock child_process.spawn for testing
import { spawn } from "child_process";
import { mock } from "bun:test";

describe("installPlugin", () => {
  test("should succeed with exit code 0", async () => {
    // Mock spawn to simulate success
    await expect(installPlugin("test-plugin")).resolves.toBeUndefined();
  });

  test("should reject on non-zero exit code", async () => {
    // Mock spawn to simulate failure
    await expect(installPlugin("bad-plugin")).rejects.toThrow(
      "Failed to install plugin 'bad-plugin' (exit code: 1)"
    );
  });

  test("should reject on spawn error", async () => {
    // Mock spawn to emit error event
    await expect(installPlugin("test-plugin")).rejects.toThrow();
  });

  test("should timeout after 5 minutes", async () => {
    // Test timeout behavior
  });
});

describe("installPlugins", () => {
  test("should handle partial failures", async () => {
    // Mock some plugins to succeed, others to fail
    await expect(installPlugins("good,bad")).rejects.toThrow(
      "Failed to install 1 plugin(s)"
    );
  });

  test("should install plugins in parallel with concurrency limit", async () => {
    // Verify concurrent installation behavior
  });
});

These tests are critical before merging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Missing test coverage for core functionality

While the parsePlugins() function has excellent test coverage (11 tests), there are zero tests for the actual plugin installation logic:

Missing tests:

  1. installPlugin() - process spawning, exit codes, error handling
  2. installPlugins() - orchestration, parallel execution, partial failures
  3. Error scenarios and edge cases

Impact: The core functionality that spawns processes and handles errors is completely untested, which poses significant risk.

Recommendation: Add tests with mocked spawn:

describe("installPlugin", () => {
  test("should resolve when plugin installs successfully (exit code 0)");
  test("should reject with error when installation fails (non-zero exit)");
  test("should reject with error on spawn error event");
  test("should handle custom claude executable");
});

describe("installPlugins", () => {
  test("should return early for empty plugin list");
  test("should install multiple plugins in parallel");
  test("should throw error if any plugin fails");
  test("should include failed plugin names in error message");
});

The syntax error on lines 47-49 of install-plugins.ts would likely be caught by these tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for critical functions

The test file only covers parsePlugins(), leaving installPlugin() and installPlugins() completely untested (0% coverage for 2 out of 3 exported functions).

Critical missing tests:

  1. installPlugin() tests (process spawning, error handling):

    • Success with exit code 0
    • Non-zero exit codes
    • Spawn errors (ENOENT, EACCES)
    • Null exit code handling
  2. installPlugins() tests (orchestration):

    • No plugins provided
    • Single and multiple plugin installation
    • Error propagation
    • Console output verification
  3. Security tests:

    • Plugin names with shell metacharacters

Recommendation: Add comprehensive tests for these functions before merging. This is critical for production reliability since these functions perform privileged operations (spawning processes).

Comment on lines +1 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Coverage: Critical functions are untested.

Current Coverage:

  • parsePlugins: 11 comprehensive tests (excellent!)
  • installPlugin: 0 tests
  • installPlugins: 0 tests

Missing Test Scenarios:

  1. Plugin installation success/failure paths
  2. Non-zero exit codes from claude CLI
  3. Process spawn errors (executable not found)
  4. Error propagation in sequential installation
  5. Empty plugin list handling in installPlugins

Recommendation: Add integration tests for the core functionality:

describe("installPlugin", () => {
  test("should reject when plugin installation fails", async () => {
    await expect(installPlugin("non-existent-plugin-xyz"))
      .rejects.toThrow("Failed to install plugin");
  });
});

describe("installPlugins", () => {
  test("should handle empty plugin list", async () => {
    await expect(installPlugins("")).resolves.toBeUndefined();
  });

  test("should report failures clearly", async () => {
    await expect(installPlugins("invalid-plugin"))
      .rejects.toThrow("Failed to install");
  });
});

These tests would catch regressions in the process spawning logic and error handling.

Loading