mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
Merge PR #134: Gemini run mode (third external-CLI mode alongside Codex/OpenCode) (COD-36)
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateSessionSchema, QuickStartSchema } from '../src/web/schemas.js';
|
||||
|
||||
describe('Gemini mode schemas', () => {
|
||||
it('accepts Gemini session creation config', () => {
|
||||
const parsed = CreateSessionSchema.parse({
|
||||
workingDir: '/tmp',
|
||||
mode: 'gemini',
|
||||
geminiConfig: {
|
||||
model: 'gemini-2.5-pro',
|
||||
approvalMode: 'yolo',
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.mode).toBe('gemini');
|
||||
expect(parsed.geminiConfig).toEqual({
|
||||
model: 'gemini-2.5-pro',
|
||||
approvalMode: 'yolo',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts Gemini quick-start config', () => {
|
||||
const parsed = QuickStartSchema.parse({
|
||||
caseName: 'gemini-case',
|
||||
mode: 'gemini',
|
||||
geminiConfig: {
|
||||
model: 'gemini-2.5-flash',
|
||||
approvalMode: 'auto_edit',
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.mode).toBe('gemini');
|
||||
expect(parsed.geminiConfig?.model).toBe('gemini-2.5-flash');
|
||||
});
|
||||
|
||||
it('rejects unsafe Gemini model strings', () => {
|
||||
expect(() =>
|
||||
CreateSessionSchema.parse({
|
||||
workingDir: '/tmp',
|
||||
mode: 'gemini',
|
||||
geminiConfig: { model: 'gemini; rm -rf /' },
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -76,11 +76,17 @@ vi.mock('../../src/utils/opencode-cli-resolver.js', () => ({
|
||||
resolveOpenCodeDir: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/utils/gemini-cli-resolver.js', () => ({
|
||||
isGeminiAvailable: vi.fn(() => false),
|
||||
resolveGeminiDir: vi.fn(() => null),
|
||||
}));
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { subagentWatcher } from '../../src/subagent-watcher.js';
|
||||
import { getLifecycleLog } from '../../src/session-lifecycle-log.js';
|
||||
import { isOpenCodeAvailable, resolveOpenCodeDir } from '../../src/utils/opencode-cli-resolver.js';
|
||||
import { isGeminiAvailable, resolveGeminiDir } from '../../src/utils/gemini-cli-resolver.js';
|
||||
|
||||
const mockedReadFile = vi.mocked(fs.readFile);
|
||||
const mockedWriteFile = vi.mocked(fs.writeFile);
|
||||
@@ -90,6 +96,8 @@ const mockedSubagentWatcher = vi.mocked(subagentWatcher);
|
||||
const mockedGetLifecycleLog = vi.mocked(getLifecycleLog);
|
||||
const mockedIsOpenCodeAvailable = vi.mocked(isOpenCodeAvailable);
|
||||
const mockedResolveOpenCodeDir = vi.mocked(resolveOpenCodeDir);
|
||||
const mockedIsGeminiAvailable = vi.mocked(isGeminiAvailable);
|
||||
const mockedResolveGeminiDir = vi.mocked(resolveGeminiDir);
|
||||
|
||||
describe('system-routes', () => {
|
||||
let harness: RouteTestHarness;
|
||||
@@ -117,6 +125,8 @@ describe('system-routes', () => {
|
||||
} as never);
|
||||
mockedIsOpenCodeAvailable.mockReturnValue(false);
|
||||
mockedResolveOpenCodeDir.mockReturnValue(null);
|
||||
mockedIsGeminiAvailable.mockReturnValue(false);
|
||||
mockedResolveGeminiDir.mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -167,7 +177,7 @@ describe('system-routes', () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.message ?? body.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -422,7 +432,7 @@ describe('system-routes', () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.message ?? body.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('saves lastUsedCase as partial update without overwriting other settings', async () => {
|
||||
@@ -520,7 +530,7 @@ describe('system-routes', () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.message ?? body.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -577,7 +587,7 @@ describe('system-routes', () => {
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.success).toBe(false);
|
||||
expect(body.message ?? body.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -765,6 +775,32 @@ describe('system-routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ========== GET /api/gemini/status ==========
|
||||
|
||||
describe('GET /api/gemini/status', () => {
|
||||
it('returns unavailable when gemini is not installed', async () => {
|
||||
mockedIsGeminiAvailable.mockReturnValue(false);
|
||||
mockedResolveGeminiDir.mockReturnValue(null);
|
||||
|
||||
const res = await harness.app.inject({ method: 'GET', url: '/api/gemini/status' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.available).toBe(false);
|
||||
expect(body.path).toBeNull();
|
||||
});
|
||||
|
||||
it('returns available with path when gemini is installed', async () => {
|
||||
mockedIsGeminiAvailable.mockReturnValue(true);
|
||||
mockedResolveGeminiDir.mockReturnValue('/usr/local/bin');
|
||||
|
||||
const res = await harness.app.inject({ method: 'GET', url: '/api/gemini/status' });
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.available).toBe(true);
|
||||
expect(body.path).toBe('/usr/local/bin');
|
||||
});
|
||||
});
|
||||
|
||||
// ========== GET /api/execution/model-config ==========
|
||||
|
||||
describe('GET /api/execution/model-config', () => {
|
||||
|
||||
@@ -61,6 +61,16 @@ describe('run mode UI', () => {
|
||||
expect(app.runMode).toBe('claude');
|
||||
expect(runBtnLabel.textContent).toBe('Run');
|
||||
});
|
||||
|
||||
it('accepts Gemini mode from server sync and updates the run button label', async () => {
|
||||
const { app, storage, runBtnLabel } = loadRunModeHarness();
|
||||
|
||||
storage.set('codeman_runMode', 'claude');
|
||||
await app.loadAppSettingsFromServer(Promise.resolve({ runMode: 'gemini' }));
|
||||
|
||||
expect(app.runMode).toBe('gemini');
|
||||
expect(runBtnLabel.textContent).toBe('Run GM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Codex quick start settings', () => {
|
||||
|
||||
Reference in New Issue
Block a user