fix(droid): avoid interactive model arg prompt pollution

- persist selector in ~/.factory/settings.json as settings.model

- stop injecting '-m custom:...' for interactive droid launches

- improve provider inference for localhost /v1 and qwen/deepseek/kimi

- add regression tests for adapter, provider, and config manager
This commit is contained in:
Tam Nhu Tran committed 2026-02-25 22:14:00 +07:00
1 parent 60b1c043c7
commit f1567c0090
8 files changed
+65 -13

No files matched your search

+2 -2
View File
@@ -5,9 +5,9 @@ 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', () => {
it('passes user args without model injection for valid profile names', () => {
const adapter = new DroidAdapter();
expect(adapter.buildArgs('gemini_01', ['--help'])).toEqual(['-m', 'custom:ccs-gemini_01', '--help']);
expect(adapter.buildArgs('gemini_01', ['--help'])).toEqual(['--help']);
});
it('rejects unsafe profile names', () => {
@@ -45,6 +45,10 @@ describe('droid-config-manager', () => {
expect(ref.selectorAlias).toBe('CCS-gemini-0');
expect(ref.selector).toBe('custom:CCS-gemini-0');
expect(ref.index).toBe(0);
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.model).toBe('custom:CCS-gemini-0');
});
it('should create settings.json with customModels', async () => {
@@ -63,6 +67,7 @@ describe('droid-config-manager', () => {
expect(settings.customModels).toHaveLength(1);
expect(settings.customModels[0].displayName).toBe('CCS gemini');
expect(settings.customModels[0].baseUrl).toBe('http://localhost:8317');
expect(settings.model).toBe('custom:CCS-gemini-0');
});
it('should update existing entry on second upsert', async () => {
+18
View File
@@ -46,6 +46,18 @@ describe('droid-provider', () => {
'generic-chat-completion-api'
);
});
it('detects localhost openai-compatible /v1 endpoints', () => {
expect(inferDroidProviderFromBaseUrl('http://127.0.0.1:1234/v1')).toBe(
'generic-chat-completion-api'
);
expect(inferDroidProviderFromBaseUrl('http://localhost:8317/v1/chat/completions')).toBe(
'generic-chat-completion-api'
);
expect(inferDroidProviderFromBaseUrl('http://[::1]:8317/v1')).toBe(
'generic-chat-completion-api'
);
});
});
describe('inferDroidProviderFromModel', () => {
@@ -56,6 +68,12 @@ describe('droid-provider', () => {
it('detects openai model naming', () => {
expect(inferDroidProviderFromModel('gpt-5-codex')).toBe('openai');
});
it('detects generic openai-compatible model families', () => {
expect(inferDroidProviderFromModel('qwen3-coder-plus')).toBe('generic-chat-completion-api');
expect(inferDroidProviderFromModel('deepseek-v3.1')).toBe('generic-chat-completion-api');
expect(inferDroidProviderFromModel('kimi-k2')).toBe('generic-chat-completion-api');
});
});
describe('resolveDroidProvider', () => {
+9 -3
View File
@@ -125,10 +125,16 @@ describe('DroidAdapter', () => {
expect(adapter.supportsProfileType('copilot')).toBe(false);
});
it('should build args with -m custom:ccs- prefix', () => {
it('should keep interactive args clean (no model argv injection)', () => {
const isolatedAdapter = new DroidAdapter();
const args = isolatedAdapter.buildArgs('gemini', ['--verbose']);
expect(args).toEqual(['-m', 'custom:ccs-gemini', '--verbose']);
expect(args).toEqual(['--verbose']);
});
it('should not queue model selector as prompt when no user args', () => {
const isolatedAdapter = new DroidAdapter();
const args = isolatedAdapter.buildArgs('codex', []);
expect(args).toEqual([]);
});
it('should build minimal env (no ANTHROPIC_ vars)', () => {
@@ -200,7 +206,7 @@ describe('DroidAdapter', () => {
});
const args = isolatedAdapter.buildArgs('gemini', ['--verbose']);
expect(args).toEqual(['-m', 'custom:CCS-gemini-0', '--verbose']);
expect(args).toEqual(['--verbose']);
} finally {
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;