Files
Codeman/test/pi-mode.test.ts
T
Codeman maintainer f4dcfbe6ca fix(pi): close four mode-list gaps in the pi run mode
Review follow-ups on #282. All four are the same failure shape: a list that
enumerates run modes, missed by the sweep that added 'pi'.

1. Cron ignored pi's project-trust clamp. The PR widened CronJobBaseSchema's
   agentType to accept 'pi' but not the matching clamp beside gemini's, so a
   non-granted multi-user owner's cron pi job spawned bare `pi` (pi's own
   defaultProjectTrust, an interactive prompt they can answer "yes" to, which
   loads and EXECUTES repo-local .pi/extensions TypeScript) while the same
   user's UI/API launch was forced to --no-approve. The clamp is now a pure
   exported helper, clampCronExternalCliConfigs(), so both it and gemini's
   previously untested materialization are pinned.

2. POST /api/sessions/:id/interactive auto-enabled the Ralph tracker for pi:
   its denylist covered opencode/codex/gemini/antigravity only. The tracker is
   never fed for an external CLI (_processExpensiveParsers returns early), so a
   pi session reported ralphEnabled and Ralph UI state no sibling backend shows.

3. REMOTE_CLI_BIN had no pi entry, so buildRemoteCliVersionProbeCommand()
   returned null and Session.cliVersion stayed blank for every remote-SSH pi
   session, even though the PR wired the remote launch command and the
   per-mode override schema field.

4. The desktop home rail's badge map had no pi entry, and its lookup falls back
   to '', which is what claude renders. A pi session read as Claude there while
   the tab strip and phone overview badged it correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:23:27 +02:00

201 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { CreateSessionSchema, QuickStartSchema } from '../src/web/schemas.js';
import { buildSpawnCommand } from '../src/tmux-manager.js';
import { defaultDockerCommandForMode } from '../src/docker-hosts.js';
import { defaultRemoteCommandForMode, buildRemoteCliVersionProbeCommand } from '../src/remote-hosts.js';
import { isExternalCliMode, isAltScreenStripMode } from '../src/session.js';
describe('Pi mode schemas', () => {
it('accepts Pi session creation config', () => {
const parsed = CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: {
model: 'sonnet:high',
provider: 'anthropic',
thinking: 'high',
},
});
expect(parsed.mode).toBe('pi');
expect(parsed.piConfig).toEqual({
model: 'sonnet:high',
provider: 'anthropic',
thinking: 'high',
});
});
it('accepts Pi quick-start config', () => {
const parsed = QuickStartSchema.parse({
caseName: 'pi-case',
mode: 'pi',
piConfig: { resumeSessionId: '0f9c2b14-aa10', continueSession: true },
});
expect(parsed.mode).toBe('pi');
expect(parsed.piConfig?.resumeSessionId).toBe('0f9c2b14-aa10');
});
it('accepts a provider-qualified model (`openai/gpt-4o`)', () => {
const parsed = CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: { model: 'openai/gpt-4o' },
});
expect(parsed.piConfig?.model).toBe('openai/gpt-4o');
});
it('rejects unsafe Pi model strings', () => {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: { model: 'pi; rm -rf /' },
})
).toThrow();
});
it('rejects unsafe Pi provider strings', () => {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: { provider: 'anthropic`whoami`' },
})
).toThrow();
});
it('rejects unsafe Pi resumeSessionId values (ids only, never paths)', () => {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: { resumeSessionId: '../../etc/passwd' },
})
).toThrow();
});
it('rejects thinking levels outside pi’s enum', () => {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
piConfig: { thinking: 'ultra' },
})
).toThrow();
});
it('allows PI_* env overrides but NOT bare provider keys', () => {
const parsed = CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
envOverrides: { PI_OFFLINE: '1' },
});
expect(parsed.envOverrides).toEqual({ PI_OFFLINE: '1' });
// Pi's ~34 provider key vars share no prefix, and ALLOWED_ENV_PREFIXES is a single
// GLOBAL list with no mode context — allowlisting them for pi would widen the
// allowlist for every mode at once. They stay out; auth goes through pi's /login.
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'pi',
envOverrides: { ANTHROPIC_API_KEY: 'sk-test' },
})
).toThrow();
});
});
describe('Pi spawn command', () => {
it('builds a bare pi command when no config is sent (pi has no permission prompts)', () => {
const cmd = buildSpawnCommand({ mode: 'pi', sessionId: 'abc12345' });
expect(cmd).toBe('pi');
});
it('maps model/provider/thinking to flags', () => {
const cmd = buildSpawnCommand({
mode: 'pi',
sessionId: 'abc12345',
piConfig: { model: 'sonnet:high', provider: 'anthropic', thinking: 'xhigh' },
});
expect(cmd).toBe('pi --model sonnet:high --provider anthropic --thinking xhigh');
});
it('emits --approve for true and --no-approve for false (tri-state project trust)', () => {
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { approveProjectTrust: true } })).toBe(
'pi --approve'
);
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { approveProjectTrust: false } })).toBe(
'pi --no-approve'
);
// Absent = pi's own defaultProjectTrust; Codeman must not decide it.
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: {} })).toBe('pi');
});
it('passes --session for resume and skips -c when both are present', () => {
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { resumeSessionId: '0f9c2b14' } })).toBe(
'pi --session 0f9c2b14'
);
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { continueSession: true } })).toBe('pi -c');
// The two conflict upstream: a valid explicit session id wins.
expect(
buildSpawnCommand({
mode: 'pi',
sessionId: 'a',
piConfig: { continueSession: true, resumeSessionId: '0f9c2b14' },
})
).toBe('pi --session 0f9c2b14');
});
it('drops unsafe values rather than escaping them (the result lands in `bash -c "..."`)', () => {
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { model: 'a`b' } })).toBe('pi');
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { provider: 'x;id' } })).toBe('pi');
expect(buildSpawnCommand({ mode: 'pi', sessionId: 'a', piConfig: { resumeSessionId: 'x; rm -rf /' } })).toBe('pi');
// An out-of-enum thinking level never reaches the command line either.
expect(
buildSpawnCommand({
mode: 'pi',
sessionId: 'a',
piConfig: { thinking: 'ultra' as unknown as 'high' },
})
).toBe('pi');
});
it('never emits --api-key (a provider secret must not reach the spawn line)', () => {
const cmd = buildSpawnCommand({
mode: 'pi',
sessionId: 'a',
piConfig: { model: 'sonnet', provider: 'anthropic', approveProjectTrust: true },
});
expect(cmd).not.toContain('--api-key');
});
});
describe('Pi mode gates', () => {
it('is an external CLI mode (readiness/ralph/respawn gating)', () => {
expect(isExternalCliMode('pi')).toBe(true);
});
it('is NOT an alt-screen strip mode (main-screen TUI + runtime-switchable fullscreen)', () => {
expect(isAltScreenStripMode('pi')).toBe(false);
});
it('has docker/remote default commands', () => {
expect(defaultDockerCommandForMode('pi')).toBe('exec pi');
// Routed through an interactive login shell so npm's global bin resolves —
// same fix as the other remote agent CLIs (see defaultRemoteCommandForMode).
expect(defaultRemoteCommandForMode('pi')).toBe('exec "${SHELL:-/bin/sh}" -i -l -c \'pi\'');
});
it('probes the CLI version on a remote host (REMOTE_CLI_BIN carries pi)', () => {
// Without the REMOTE_CLI_BIN entry this returns null and Session.cliVersion stays
// blank for every remote pi session, which is invisible until someone asks why the
// version column is empty on that host only.
const cmd = buildRemoteCliVersionProbeCommand({ username: 'dev', host: 'box.example', port: 22 }, 'pi');
expect(cmd).not.toBeNull();
expect(cmd).toContain('pi --version');
});
});