fix: address PR review findings for Gemini run mode + Ralph todo-config

Gemini (PR #134) blockers:
- runGemini() now unwraps the {success,data} envelope: status check reads
  .data.available, quick-start reads data.data.sessionId (was reading the raw
  shape, so the Run-Gemini button could never start a session).
- setGeminiEnvVars() now uses the socket-scoped ${this.tmux()} setenv instead of
  bare tmux — Gemini/Google auth env vars were targeting the wrong tmux server
  and silently failing on every install.

Gemini parity polish:
- gemini tab-mode badge ('gm') + .tab-mode.gemini CSS; kill-dialog label
  'Kill Tmux & Gemini'; codeman doctor dependency-registry entry; export
  isGeminiAvailable from utils barrel; COLORTERM=truecolor + unset NO_COLOR;
  add gemini to isAltScreenStripMode (Ink TUI, repaints inline like Codex/Claude).
- Revert 4 system-routes.test.ts envelope assertions weakened to
  (body.message ?? body.error) back to (body.success === false).
- Add a runGemini() vm-sandbox test that drives the envelope path end-to-end.

Ralph todo-config (PR #135): maxTodos/todoExpirationMinutes are now persisted
and read back — surfaced via the loopState getter (RalphTrackerState) into
toState()/SSE broadcast and restored in restoreState(), mirroring maxIterations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-06-25 00:26:06 +02:00
parent 9cc310e843
commit 368fc20fc2
11 changed files with 106 additions and 21 deletions
+4 -4
View File
@@ -177,7 +177,7 @@ describe('system-routes', () => {
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.message ?? body.error).toBeTruthy();
expect(body.success).toBe(false);
});
});
@@ -432,7 +432,7 @@ describe('system-routes', () => {
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.message ?? body.error).toBeTruthy();
expect(body.success).toBe(false);
});
it('saves lastUsedCase as partial update without overwriting other settings', async () => {
@@ -530,7 +530,7 @@ describe('system-routes', () => {
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.message ?? body.error).toBeTruthy();
expect(body.success).toBe(false);
});
});
@@ -587,7 +587,7 @@ describe('system-routes', () => {
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.message ?? body.error).toBeTruthy();
expect(body.success).toBe(false);
});
});
+53
View File
@@ -143,3 +143,56 @@ describe('Codex quick start settings', () => {
expect(selected).toEqual(['sess-1']);
});
});
describe('Gemini quick start', () => {
// Regression guard for the ApiResponse-envelope unwrap in runGemini(): the
// status check must read `.data.available` and the quick-start response must
// read `.data.sessionId`. Reading the raw shape (pre-fix) silently bails on
// the status check and never selects the new tab — exactly the two blockers
// caught in PR #134 review.
it('drives runGemini() through the {success,data} envelope and selects the new session', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'gemini-case' },
};
const requests: Array<{ url: string; body?: any }> = [];
const CodemanApp = function CodemanApp(this: any) {};
const context = vm.createContext({
CodemanApp,
localStorage: { getItem: () => null, setItem: () => {} },
document: { getElementById: (id: string) => elements[id] ?? null },
// Mock responses use the real wire shape: the server.ts preSerialization
// hook wraps raw route payloads into the { success, data } envelope.
fetch: async (url: string, init?: { body?: string }) => {
requests.push({ url, body: init?.body ? JSON.parse(init.body) : undefined });
if (url === '/api/gemini/status') return { json: async () => ({ success: true, data: { available: true } }) };
if (url === '/api/quick-start')
return { json: async () => ({ success: true, data: { sessionId: 'sess-gm' } }) };
throw new Error(`unexpected fetch: ${url}`);
},
console,
});
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
const app = new (CodemanApp as any)();
app.terminal = { clear: () => {}, writeln: () => {}, focus: () => {} };
app.loadAppSettingsFromStorage = () => ({});
app.getCaseSettings = () => ({});
app.buildEnvOverrides = () => ({});
const selected: string[] = [];
app.selectSession = async (id: string) => {
selected.push(id);
};
await app.runGemini();
expect(requests.find((req) => req.url === '/api/quick-start')?.body).toMatchObject({
caseName: 'gemini-case',
mode: 'gemini',
geminiConfig: { approvalMode: 'yolo' },
});
expect(selected).toEqual(['sess-gm']);
});
});