From 812bb5c0a5b60041d1928189019cc75183ae525d Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Feb 2026 20:50:50 +0700 Subject: [PATCH] fix(targets): run cleanup before adapter launch exits - wrap adapter early-exit paths with a cleanup-aware exit helper - invoke cleanup before exiting on child spawn failures - validate Droid profile names in buildArgs and add unit coverage --- src/targets/claude-adapter.ts | 15 +++++++++++--- src/targets/droid-adapter.ts | 26 +++++++++++++++++------- tests/unit/targets/droid-adapter.test.ts | 17 ++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 tests/unit/targets/droid-adapter.test.ts diff --git a/src/targets/claude-adapter.ts b/src/targets/claude-adapter.ts index d7b60035..4fee3d80 100644 --- a/src/targets/claude-adapter.ts +++ b/src/targets/claude-adapter.ts @@ -13,6 +13,7 @@ import { escapeShellArg, stripAnthropicEnv } from '../utils/shell-executor'; import { ErrorManager } from '../utils/error-manager'; import { getWebSearchHookEnv } from '../utils/websearch-manager'; import { wireChildProcessSignals } from '../utils/signal-forwarder'; +import { runCleanup } from '../errors'; export class ClaudeAdapter implements TargetAdapter { readonly type: TargetType = 'claude'; @@ -63,11 +64,19 @@ export class ClaudeAdapter implements TargetAdapter { env: NodeJS.ProcessEnv, _options?: { cwd?: string; binaryInfo?: TargetBinaryInfo } ): void { + const exitWithCleanup = (code: number): never => { + try { + runCleanup(); + } catch { + // Cleanup should be best-effort on launch errors. + } + process.exit(code); + }; + const claudeCli = detectClaudeCli(); if (!claudeCli) { void ErrorManager.showClaudeNotFound(); - process.exit(1); - return; + return exitWithCleanup(1); } const isWindows = process.platform === 'win32'; @@ -118,7 +127,7 @@ export class ClaudeAdapter implements TargetAdapter { } else { console.error(`[X] Failed to start Claude CLI (${claudeCli}): ${err.message}`); } - process.exit(1); + return exitWithCleanup(1); }); } diff --git a/src/targets/droid-adapter.ts b/src/targets/droid-adapter.ts index 79004bd3..bb3a8008 100644 --- a/src/targets/droid-adapter.ts +++ b/src/targets/droid-adapter.ts @@ -13,6 +13,7 @@ import type { ProfileType } from '../types/profile'; import { upsertCcsModel } from './droid-config-manager'; import { escapeShellArg } from '../utils/shell-executor'; import { wireChildProcessSignals } from '../utils/signal-forwarder'; +import { runCleanup } from '../errors'; export class DroidAdapter implements TargetAdapter { readonly type: TargetType = 'droid'; @@ -52,6 +53,11 @@ export class DroidAdapter implements TargetAdapter { } buildArgs(profile: string, userArgs: string[]): string[] { + if (!/^[a-zA-Z0-9_-]+$/.test(profile)) { + throw new Error( + `Invalid profile name "${profile}" for Droid target: only alphanumeric, underscore, hyphen allowed` + ); + } return ['-m', `custom:ccs-${profile}`, ...userArgs]; } @@ -67,26 +73,32 @@ export class DroidAdapter implements TargetAdapter { env: NodeJS.ProcessEnv, options?: { cwd?: string; binaryInfo?: TargetBinaryInfo } ): void { + const exitWithCleanup = (code: number): never => { + try { + runCleanup(); + } catch { + // Cleanup should be best-effort on launch errors. + } + process.exit(code); + }; + const droidPath = options?.binaryInfo?.path || detectDroidCli(); if (!droidPath) { console.error('[X] Droid CLI not found. Install: npm i -g @factory/cli'); - process.exit(1); - return; + return exitWithCleanup(1); } try { const stat = fs.statSync(droidPath); if (!stat.isFile()) { console.error(`[X] Droid CLI path is not a file: ${droidPath}`); - process.exit(1); - return; + return exitWithCleanup(1); } } catch (err) { const error = err as NodeJS.ErrnoException; console.error( `[X] Droid CLI path is not accessible (${error.code || 'unknown'}): ${droidPath}` ); - process.exit(1); - return; + return exitWithCleanup(1); } const isWindows = process.platform === 'win32'; @@ -138,7 +150,7 @@ export class DroidAdapter implements TargetAdapter { } else { console.error(`[X] Failed to start Droid CLI (${droidPath}):`, err.message); } - process.exit(1); + return exitWithCleanup(1); }); } diff --git a/tests/unit/targets/droid-adapter.test.ts b/tests/unit/targets/droid-adapter.test.ts new file mode 100644 index 00000000..3c1501c6 --- /dev/null +++ b/tests/unit/targets/droid-adapter.test.ts @@ -0,0 +1,17 @@ +/** + * Unit tests for Droid adapter argument building. + */ +import { describe, it, expect } from 'bun:test'; +import { DroidAdapter } from '../../../src/targets/droid-adapter'; + +describe('DroidAdapter.buildArgs', () => { + it('builds droid model args for valid profile names', () => { + const adapter = new DroidAdapter(); + expect(adapter.buildArgs('gemini_01', ['--help'])).toEqual(['-m', 'custom:ccs-gemini_01', '--help']); + }); + + it('rejects unsafe profile names', () => { + const adapter = new DroidAdapter(); + expect(() => adapter.buildArgs('bad profile', [])).toThrow(/Invalid profile name/); + }); +});