Merge PR #134: Gemini run mode (third external-CLI mode alongside Codex/OpenCode) (COD-36)

This commit is contained in:
Codeman maintainer
2026-06-25 00:10:52 +02:00
17 changed files with 520 additions and 40 deletions
+40 -4
View File
@@ -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', () => {