-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: add dependency injection for clean code and TDD #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Introduce abstraction interfaces for dependency injection: - FileSystem: file operations (read, write, exists, mkdir, rm, etc.) - ProcessEnv: environment variables, cwd, homedir, pid - Shell: spawn operations for external commands These interfaces enable testability by allowing mock implementations.
Production adapters (using Bun/Node APIs): - BunFileSystem: Bun.file(), Bun.write(), fs/promises - NodeProcess: process.env, process.cwd(), os.homedir() - BunShell: Bun.spawnSync() Mock adapters for unit testing: - MemoryFileSystem: in-memory file storage with fluent builder - MockProcess: configurable env, cwd, homedir, pid - MockShell: handler-based command mocking with call tracking
- Add ConfigDependencies interface with optional fs and process - Update loadConfig/saveConfig to accept deps parameter - Default to real implementations for backward compatibility - Add 14 unit tests using mock dependencies
- Add ScannerDependencies interface with optional fs and process - Update all scan functions to accept deps parameter - Default to real implementations for backward compatibility - Add 11 unit tests using mock dependencies
- Rename MarketplacePaths to MarketplaceDependencies - Add optional fs (FileSystem) and shell (Shell) properties - Replace Bun.spawnSync with injected shell - Replace direct fs operations with injected file system - Update tests to use MemoryFileSystem and MockShell - Tests no longer touch real file system
- Add CacheDependencies and CacheInstance interfaces - Create createCache() factory that encapsulates state - Keep backward-compatible initCache/getCachedPlugin/cleanupCache - Add 16 unit tests using MemoryFileSystem and MockProcess - Rename original tests to cache.integration.test.ts
- Add PluginDependencies interface for scanner, config, output - Inject exit/log/error functions instead of direct calls - Update tests to use pure mocks (no file I/O) - Remove process.chdir and process.env.HOME manipulation - All 9 tests now pure unit tests
translator.ts: - Add TranslatorDependencies with optional cache and fs - Use createCache() instead of initCache() executor.ts: - Add ExecutorDependencies with optional shell and env - Use injected shell instead of Bun.spawnSync
Create src/test-utils/index.ts that re-exports all mock adapters: - MemoryFileSystem, createMemoryFileSystem - MockProcess, createMockProcess - MockShell, createMockShell Provides single import point for test dependencies.
Document the dependency injection patterns and test utilities: - List all modules with their dependency interfaces - Show how to import test utilities - Explain unit vs integration test conventions - Provide example unit test with mocks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces comprehensive dependency injection across all core modules to improve testability and follow SOLID principles. The refactoring adds interface abstractions for FileSystem, ProcessEnv, and Shell operations, with production adapters using Bun/Node.js APIs and test adapters using in-memory implementations.
Changes:
- Created three core interfaces (FileSystem, ProcessEnv, Shell) with production and test adapters
- Refactored 7 core modules to accept optional dependency injection parameters with production defaults
- Increased test count from 63 to 104 with in-memory mocks replacing file system modifications
- Added comprehensive test utilities and separated integration tests
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/interfaces/file-system.ts | New FileSystem interface for file operations abstraction |
| src/interfaces/process.ts | New ProcessEnv interface for environment/system info abstraction |
| src/interfaces/shell.ts | New Shell interface for command spawning abstraction |
| src/adapters/bun-file-system.ts | Production FileSystem implementation using Bun/Node.js |
| src/adapters/node-process.ts | Production ProcessEnv implementation using Node.js APIs |
| src/adapters/bun-shell.ts | Production Shell implementation using Bun.spawnSync |
| src/adapters/memory-file-system.ts | In-memory FileSystem for testing |
| src/adapters/mock-process.ts | Mock ProcessEnv for testing |
| src/adapters/mock-shell.ts | Mock Shell for testing |
| src/test-utils/index.ts | Centralized test utility exports |
| src/translator.ts | Added TranslatorDependencies with cache/fs injection |
| src/scanner.ts | Added ScannerDependencies with fs/process injection |
| src/plugin.ts | Added PluginDependencies with scanner/config/output injection |
| src/marketplace.ts | Added MarketplaceDependencies with fs/shell/paths injection |
| src/config.ts | Added ConfigDependencies with fs/process injection |
| src/cache.ts | Added CacheInstance factory pattern, removed global state |
| src/executor.ts | Added ExecutorDependencies with shell/env injection |
| src/scanner.test.ts | New unit tests using memory mocks |
| src/plugin.test.ts | Refactored to use mock dependencies |
| src/marketplace.test.ts | Refactored to use memory file system and mock shell |
| src/config.test.ts | New unit tests with memory file system |
| src/cache.unit.test.ts | New unit tests with memory file system |
| src/cache.integration.test.ts | Integration tests using real file system |
| AGENTS.md | Updated documentation with testing patterns and DI examples |
Comments suppressed due to low confidence (1)
src/marketplace.ts:44
- The getMarketplacesRoot function still directly accesses process.env.HOME and calls homedir() from node:os instead of using injected ProcessEnv dependency. This breaks the dependency injection pattern and makes the function not fully testable. Consider accepting a ProcessEnv parameter similar to how scanner.ts handles it in getKnownMarketplacesPath.
function getMarketplacesRoot(deps?: MarketplaceDependencies): string {
if (deps?.marketplacesRoot) {
return deps.marketplacesRoot;
}
const homeDir = process.env.HOME ?? homedir();
return join(homeDir, ".claude", "plugins", "marketplaces");
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Use result.stdout/stderr directly instead of accessing .buffer property. Bun.spawnSync already returns Uint8Array instances.
- Replace process.exit(1) with throw Error for testability - Remove unused 'shell' variable from addMarketplace destructuring
- Add writeFileSync and mkdirSync for use in shell mock handlers - Update marketplace tests to use sync methods instead of async - Add unit tests for new sync methods
- Update AGENTS.md example to show proc.cwd() DI pattern - Add comment explaining sync imports in cache.ts for backward compat
|
All review feedback has been addressed in commits cf4148e through 6672b4d: Fixes Applied:
New Tests Added:
All 113 tests pass ✅ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (4)
src/marketplace.ts:44
- This function still uses global
process.env.HOMEandhomedir()directly instead of using the injected dependencies. This is inconsistent with the dependency injection pattern used throughout the PR. Consider accepting aProcessEnvdependency or using the injected file system's environment access pattern.
function getMarketplacesRoot(deps?: MarketplaceDependencies): string {
if (deps?.marketplacesRoot) {
return deps.marketplacesRoot;
}
const homeDir = process.env.HOME ?? homedir();
return join(homeDir, ".claude", "plugins", "marketplaces");
}
src/marketplace.ts:150
- This function directly calls
process.exit()which makes it non-testable and breaks the dependency injection pattern. The tests work around this by mockingprocess.exit, but it would be cleaner to either throw an error that the caller can handle, or accept an exit function in the dependencies similar to the pattern used inplugin.ts.
function runGitCommand(args: string[], deps?: MarketplaceDependencies): void {
const { shell } = { ...defaultDeps, ...deps };
let result;
try {
result = shell.spawnSync(["git", ...args], {
stdout: "pipe",
stderr: "pipe",
});
} catch (error) {
console.error(
`Error: Failed to clone/update marketplace: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
if (result.exitCode !== 0 || result.exitCode === null) {
const details =
decodeOutput(result.stderr) ||
decodeOutput(result.stdout) ||
`exit code ${result.exitCode ?? "unknown"}`;
console.error(`Error: Failed to clone/update marketplace: ${details}`);
process.exit(1);
}
}
src/marketplace.ts:96
- Similar to
runGitCommand, this function directly callsprocess.exit()which breaks testability and the dependency injection pattern. Consider throwing an error instead that can be caught and handled by the caller, or accepting an exit function in the dependencies.
async function writeKnownMarketplaces(
data: KnownMarketplacesFile,
deps?: MarketplaceDependencies,
): Promise<void> {
const { fs } = { ...defaultDeps, ...deps };
const filePath = deps?.knownMarketplacesPath ?? getKnownMarketplacesPath();
try {
await fs.mkdir(dirname(filePath), { recursive: true });
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
} catch (error) {
console.error(
`Error: Failed to write known marketplaces file at "${filePath}": ${
error instanceof Error ? error.message : String(error)
}`,
);
process.exit(1);
}
}
src/marketplace.ts:77
- This function also calls
process.exit()directly. The entire marketplace.ts file has multipleprocess.exit()calls that break the dependency injection pattern established elsewhere. For consistency withplugin.tswhich properly injects exit/log/error functions, consider adding these toMarketplaceDependencies.
async function readKnownMarketplaces(
deps?: MarketplaceDependencies,
): Promise<KnownMarketplacesFile> {
const { fs } = { ...defaultDeps, ...deps };
const filePath = deps?.knownMarketplacesPath ?? getKnownMarketplacesPath();
if (!(await fs.exists(filePath))) {
return {};
}
try {
const content = await fs.readFile(filePath);
const data = JSON.parse(content);
if (typeof data === "object" && data !== null) {
return data as KnownMarketplacesFile;
}
} catch (error) {
console.error(
`Error: Failed to read known marketplaces: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
return {};
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/adapters/bun-file-system.ts
Outdated
| if (dir !== '.' && dir !== '..') { | ||
| await this.mkdir(dir, { recursive: true }); | ||
| } |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition dir !== '.' && dir !== '..' may not cover all cases where mkdir should be skipped. For example, if filePath is just a filename without a directory (like "file.txt"), dirname returns ".", but an absolute path at root like "/file.txt" would have dirname "/". Consider a more robust check or simply letting mkdir handle these cases since it's called with recursive: true.
| if (dir !== '.' && dir !== '..') { | |
| await this.mkdir(dir, { recursive: true }); | |
| } | |
| await this.mkdir(dir, { recursive: true }); |
| @@ -0,0 +1,92 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
| import { MemoryFileSystem, createMemoryFileSystem } from "./memory-file-system"; | |||
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import createMemoryFileSystem.
| import { MemoryFileSystem, createMemoryFileSystem } from "./memory-file-system"; | |
| import { MemoryFileSystem } from "./memory-file-system"; |
- bun-file-system.ts: Remove condition before mkdir since recursive:true handles all cases (root paths, relative paths) - memory-file-system.test.ts: Remove unused createMemoryFileSystem import
Summary
This PR introduces dependency injection across all core modules to improve testability and follow clean code/SOLID principles.
Changes
New Interfaces (
src/interfaces/)New Adapters (
src/adapters/)BunFileSystem,NodeProcess,BunShellMemoryFileSystem,MockProcess,MockShellRefactored Modules
ConfigDependencieswith fs/processScannerDependencieswith fs/processMarketplaceDependencieswith fs/shell/pathsCacheInstancefactory, removed global statePluginDependencieswith scanner/config/outputTranslatorDependencieswith cache/fsExecutorDependencieswith shell/envTest Improvements
process.cwd()orprocess.env.HOME*.integration.test.tsDocumentation
Breaking Changes
None - all changes are backward compatible. Functions accept optional
depsparameter with production defaults.