mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-18 14:13:21 +00:00
fix(droid): harden exec passthrough and add integration coverage
This commit is contained in:
1 parent
5963ba973e
commit
ce44d2ece0
6 files changed
+441
-42
No files matched your search
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { routeDroidCommandArgs } from '../../../src/targets/droid-command-router';
|
||||
import {
|
||||
DroidCommandRouterError,
|
||||
routeDroidCommandArgs,
|
||||
} from '../../../src/targets/droid-command-router';
|
||||
|
||||
describe('droid-command-router', () => {
|
||||
it('keeps interactive mode for plain profile launches', () => {
|
||||
@@ -8,6 +11,7 @@ describe('droid-command-router', () => {
|
||||
expect(route.mode).toBe('interactive');
|
||||
expect(route.argsForDroid).toEqual([]);
|
||||
expect(route.autoPrependedExec).toBe(false);
|
||||
expect(route.duplicateReasoningDisplays).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps explicit droid subcommands untouched', () => {
|
||||
@@ -17,6 +21,7 @@ describe('droid-command-router', () => {
|
||||
expect(route.command).toBe('mcp');
|
||||
expect(route.argsForDroid).toEqual(['mcp', '--help']);
|
||||
expect(route.autoPrependedExec).toBe(false);
|
||||
expect(route.duplicateReasoningDisplays).toEqual([]);
|
||||
});
|
||||
|
||||
it('auto-prepends exec for exec-only flags provided after profile', () => {
|
||||
@@ -26,6 +31,7 @@ describe('droid-command-router', () => {
|
||||
expect(route.command).toBe('exec');
|
||||
expect(route.argsForDroid).toEqual(['exec', '--skip-permissions-unsafe']);
|
||||
expect(route.autoPrependedExec).toBe(true);
|
||||
expect(route.duplicateReasoningDisplays).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not auto-prepend exec for root help flag', () => {
|
||||
@@ -34,6 +40,7 @@ describe('droid-command-router', () => {
|
||||
expect(route.mode).toBe('interactive');
|
||||
expect(route.argsForDroid).toEqual(['--help']);
|
||||
expect(route.autoPrependedExec).toBe(false);
|
||||
expect(route.duplicateReasoningDisplays).toEqual([]);
|
||||
});
|
||||
|
||||
it('normalizes --effort alias to --reasoning-effort for explicit exec', () => {
|
||||
@@ -41,12 +48,8 @@ describe('droid-command-router', () => {
|
||||
|
||||
expect(route.mode).toBe('command');
|
||||
expect(route.command).toBe('exec');
|
||||
expect(route.argsForDroid).toEqual([
|
||||
'exec',
|
||||
'--reasoning-effort',
|
||||
'xhigh',
|
||||
'fix test flake',
|
||||
]);
|
||||
expect(route.argsForDroid).toEqual(['exec', '--reasoning-effort', 'xhigh', 'fix test flake']);
|
||||
expect(route.reasoningSourceDisplay).toBe('--effort xhigh');
|
||||
});
|
||||
|
||||
it('normalizes --thinking alias when exec is auto-prepended', () => {
|
||||
@@ -58,10 +61,12 @@ describe('droid-command-router', () => {
|
||||
'exec',
|
||||
'--auto',
|
||||
'high',
|
||||
'--reasoning-effort=medium',
|
||||
'--reasoning-effort',
|
||||
'medium',
|
||||
'summarize logs',
|
||||
]);
|
||||
expect(route.autoPrependedExec).toBe(true);
|
||||
expect(route.reasoningSourceDisplay).toBe('--thinking=medium');
|
||||
});
|
||||
|
||||
it('still auto-prepends exec when --effort appears before exec-only flags', () => {
|
||||
@@ -82,5 +87,80 @@ describe('droid-command-router', () => {
|
||||
'fix flaky test',
|
||||
]);
|
||||
expect(route.autoPrependedExec).toBe(true);
|
||||
expect(route.reasoningSourceDisplay).toBe('--effort xhigh');
|
||||
});
|
||||
|
||||
it('auto-prepends exec for non-ambiguous short exec flags', () => {
|
||||
const route = routeDroidCommandArgs(['-m', 'custom:gpt-5.3-codex', 'fix flaky test']);
|
||||
|
||||
expect(route.mode).toBe('command');
|
||||
expect(route.command).toBe('exec');
|
||||
expect(route.argsForDroid).toEqual(['exec', '-m', 'custom:gpt-5.3-codex', 'fix flaky test']);
|
||||
expect(route.autoPrependedExec).toBe(true);
|
||||
});
|
||||
|
||||
it('routes -r to exec when value matches reasoning effort level', () => {
|
||||
const route = routeDroidCommandArgs(['-r', 'high', 'summarize logs']);
|
||||
|
||||
expect(route.mode).toBe('command');
|
||||
expect(route.command).toBe('exec');
|
||||
expect(route.argsForDroid).toEqual(['exec', '--reasoning-effort', 'high', 'summarize logs']);
|
||||
expect(route.autoPrependedExec).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps interactive mode for ambiguous -r resume-style usage', () => {
|
||||
const route = routeDroidCommandArgs(['-r', 'session-1234']);
|
||||
|
||||
expect(route.mode).toBe('interactive');
|
||||
expect(route.argsForDroid).toEqual(['-r', 'session-1234']);
|
||||
expect(route.autoPrependedExec).toBe(false);
|
||||
});
|
||||
|
||||
it('dedupes mixed reasoning flags with first occurrence precedence', () => {
|
||||
const route = routeDroidCommandArgs([
|
||||
'exec',
|
||||
'--reasoning-effort',
|
||||
'high',
|
||||
'--thinking',
|
||||
'low',
|
||||
'--effort=xhigh',
|
||||
'summarize logs',
|
||||
]);
|
||||
|
||||
expect(route.mode).toBe('command');
|
||||
expect(route.command).toBe('exec');
|
||||
expect(route.argsForDroid).toEqual(['exec', '--reasoning-effort', 'high', 'summarize logs']);
|
||||
expect(route.reasoningSourceDisplay).toBe('--reasoning-effort high');
|
||||
expect(route.duplicateReasoningDisplays).toEqual(['--thinking low', '--effort=xhigh']);
|
||||
});
|
||||
|
||||
it('throws for missing reasoning value in command mode (alias)', () => {
|
||||
expect(() => routeDroidCommandArgs(['exec', '--effort'])).toThrow(DroidCommandRouterError);
|
||||
});
|
||||
|
||||
it('throws for missing reasoning value in command mode (native)', () => {
|
||||
expect(() => routeDroidCommandArgs(['exec', '--reasoning-effort'])).toThrow(
|
||||
DroidCommandRouterError
|
||||
);
|
||||
});
|
||||
|
||||
it('records malformed duplicate reasoning flags when first value is already selected', () => {
|
||||
const route = routeDroidCommandArgs([
|
||||
'exec',
|
||||
'--thinking',
|
||||
'medium',
|
||||
'--reasoning-effort',
|
||||
'--skip-permissions-unsafe',
|
||||
'summarize logs',
|
||||
]);
|
||||
|
||||
expect(route.argsForDroid).toEqual([
|
||||
'exec',
|
||||
'--reasoning-effort',
|
||||
'medium',
|
||||
'--skip-permissions-unsafe',
|
||||
'summarize logs',
|
||||
]);
|
||||
expect(route.duplicateReasoningDisplays).toEqual(['--reasoning-effort <missing-value>']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
interface RunResult {
|
||||
status: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
function runCcs(args: string[], env: NodeJS.ProcessEnv): RunResult {
|
||||
const ccsEntry = path.join(process.cwd(), 'src', 'ccs.ts');
|
||||
const result = spawnSync(process.execPath, [ccsEntry, ...args], {
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
stdout: result.stdout || '',
|
||||
stderr: result.stderr || '',
|
||||
};
|
||||
}
|
||||
|
||||
describe('droid command routing integration', () => {
|
||||
let tmpHome: string;
|
||||
let ccsDir: string;
|
||||
let settingsPath: string;
|
||||
let configPath: string;
|
||||
let fakeDroidPath: string;
|
||||
let droidArgsLogPath: string;
|
||||
let baseEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-droid-route-it-'));
|
||||
ccsDir = path.join(tmpHome, '.ccs');
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
|
||||
settingsPath = path.join(ccsDir, 'myglm.settings.json');
|
||||
configPath = path.join(ccsDir, 'config.json');
|
||||
fakeDroidPath = path.join(tmpHome, 'fake-droid.js');
|
||||
droidArgsLogPath = path.join(tmpHome, 'droid-args.json');
|
||||
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'https://example.invalid/anthropic',
|
||||
ANTHROPIC_AUTH_TOKEN: 'test-token',
|
||||
ANTHROPIC_MODEL: 'gpt-5.3-codex',
|
||||
CCS_DROID_PROVIDER: 'openai',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
profiles: {
|
||||
myglm: settingsPath,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
fakeDroidPath,
|
||||
`#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const out = process.env.CCS_TEST_DROID_ARGS_OUT;
|
||||
if (!out) process.exit(2);
|
||||
fs.writeFileSync(out, JSON.stringify(process.argv.slice(2)));
|
||||
process.exit(0);
|
||||
`,
|
||||
{ encoding: 'utf8', mode: 0o755 }
|
||||
);
|
||||
fs.chmodSync(fakeDroidPath, 0o755);
|
||||
|
||||
baseEnv = {
|
||||
...process.env,
|
||||
CI: '1',
|
||||
NO_COLOR: '1',
|
||||
CCS_HOME: tmpHome,
|
||||
CCS_DROID_PATH: fakeDroidPath,
|
||||
CCS_TEST_DROID_ARGS_OUT: droidArgsLogPath,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (process.platform === 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('auto-routes exec-only long flags to droid exec from main ccs flow', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcs(
|
||||
['myglm', '--target', 'droid', '--skip-permissions-unsafe', 'fix failing tests'],
|
||||
baseEnv
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
const routedArgs = JSON.parse(fs.readFileSync(droidArgsLogPath, 'utf8')) as string[];
|
||||
expect(routedArgs).toEqual(['exec', '--skip-permissions-unsafe', 'fix failing tests']);
|
||||
});
|
||||
|
||||
it('auto-routes non-ambiguous short exec flags', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcs(
|
||||
['myglm', '--target', 'droid', '-m', 'custom:gpt-5.3-codex', 'fix failing tests'],
|
||||
baseEnv
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
const routedArgs = JSON.parse(fs.readFileSync(droidArgsLogPath, 'utf8')) as string[];
|
||||
expect(routedArgs).toEqual(['exec', '-m', 'custom:gpt-5.3-codex', 'fix failing tests']);
|
||||
});
|
||||
|
||||
it('dedupes reasoning flags with first occurrence precedence in exec mode', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcs(
|
||||
[
|
||||
'myglm',
|
||||
'--target',
|
||||
'droid',
|
||||
'exec',
|
||||
'--reasoning-effort',
|
||||
'high',
|
||||
'--thinking',
|
||||
'low',
|
||||
'summarize logs',
|
||||
],
|
||||
baseEnv
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toContain('Multiple reasoning flags detected');
|
||||
const routedArgs = JSON.parse(fs.readFileSync(droidArgsLogPath, 'utf8')) as string[];
|
||||
expect(routedArgs).toEqual(['exec', '--reasoning-effort', 'high', 'summarize logs']);
|
||||
});
|
||||
|
||||
it('fails fast for malformed reasoning alias in command mode', () => {
|
||||
if (process.platform === 'win32') return;
|
||||
|
||||
const result = runCcs(['myglm', '--target', 'droid', 'exec', '--effort'], baseEnv);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain('--effort requires a value');
|
||||
expect(fs.existsSync(droidArgsLogPath)).toBe(true);
|
||||
const probeArgs = JSON.parse(fs.readFileSync(droidArgsLogPath, 'utf8')) as string[];
|
||||
// Droid binary is still invoked once for version preflight (`--version`) before routing.
|
||||
expect(probeArgs).toEqual(['--version']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user