feat(team): make tmux optional for desktop teammates Merge pull request #89 from 777genius/feat/tmux-optional-process-backend
Merge pull request #89 from 777genius/feat/tmux-optional-process-backend
This commit is contained in:
commit
2c2f84a00e
7 changed files with 209 additions and 45 deletions
|
|
@ -186,7 +186,11 @@ import {
|
|||
buildProgressAssistantOutput,
|
||||
buildProgressLogsTail,
|
||||
} from './progressPayload';
|
||||
import { resolveDesktopTeammateModeDecision } from './runtimeTeammateMode';
|
||||
import {
|
||||
applyDesktopTeammateModeDecisionToEnv,
|
||||
buildDesktopTeammateModeCliArgs,
|
||||
resolveDesktopTeammateModeDecision,
|
||||
} from './runtimeTeammateMode';
|
||||
import {
|
||||
choosePreferredLaunchSnapshot,
|
||||
clearBootstrapState,
|
||||
|
|
@ -1101,6 +1105,9 @@ function buildRuntimeLaunchWarning(
|
|||
if (env.CLAUDE_CODE_CODEX_BACKEND) {
|
||||
flags.push(`CODEX_BACKEND=${env.CLAUDE_CODE_CODEX_BACKEND}`);
|
||||
}
|
||||
if (env.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES === '1') {
|
||||
flags.push('FORCE_PROCESS_TEAMMATES');
|
||||
}
|
||||
const backendPart = backend ? `, backend ${backend}` : '';
|
||||
const flagsPart = flags.length > 0 ? `, env ${flags.join(', ')}` : '';
|
||||
const geminiAuth = options?.geminiRuntimeAuth;
|
||||
|
|
@ -1164,6 +1171,7 @@ function logRuntimeLaunchSnapshot(
|
|||
CLAUDE_CODE_ENTRY_PROVIDER: env.CLAUDE_CODE_ENTRY_PROVIDER ?? null,
|
||||
CLAUDE_CODE_GEMINI_BACKEND: env.CLAUDE_CODE_GEMINI_BACKEND ?? null,
|
||||
CLAUDE_CODE_CODEX_BACKEND: env.CLAUDE_CODE_CODEX_BACKEND ?? null,
|
||||
CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES: env.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES ?? null,
|
||||
CLAUDE_CONFIG_DIR: env.CLAUDE_CONFIG_DIR ?? null,
|
||||
CLAUDE_TEAM_CONTROL_URL: env.CLAUDE_TEAM_CONTROL_URL ?? null,
|
||||
},
|
||||
|
|
@ -11788,9 +11796,7 @@ export class TeamProvisioningService {
|
|||
let child: ReturnType<typeof spawn>;
|
||||
shellEnv.CLAUDE_ENABLE_DETERMINISTIC_TEAM_BOOTSTRAP = '1';
|
||||
const teammateModeDecision = await resolveDesktopTeammateModeDecision(request.extraCliArgs);
|
||||
if (teammateModeDecision.forceProcessTeammates) {
|
||||
shellEnv.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES = '1';
|
||||
}
|
||||
applyDesktopTeammateModeDecisionToEnv(shellEnv, teammateModeDecision);
|
||||
let mcpConfigPath: string;
|
||||
let bootstrapSpecPath: string;
|
||||
let bootstrapUserPromptPath: string | null = null;
|
||||
|
|
@ -11854,6 +11860,7 @@ export class TeamProvisioningService {
|
|||
...(launchIdentity.resolvedEffort ? ['--effort', launchIdentity.resolvedEffort] : []),
|
||||
...providerFastModeArgs,
|
||||
...(request.worktree ? ['--worktree', request.worktree] : []),
|
||||
...buildDesktopTeammateModeCliArgs(teammateModeDecision),
|
||||
...parseCliArgs(request.extraCliArgs),
|
||||
...providerArgs,
|
||||
]);
|
||||
|
|
@ -12863,9 +12870,7 @@ export class TeamProvisioningService {
|
|||
let child: ReturnType<typeof spawn>;
|
||||
shellEnv.CLAUDE_ENABLE_DETERMINISTIC_TEAM_BOOTSTRAP = '1';
|
||||
const teammateModeDecision = await resolveDesktopTeammateModeDecision(request.extraCliArgs);
|
||||
if (teammateModeDecision.forceProcessTeammates) {
|
||||
shellEnv.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES = '1';
|
||||
}
|
||||
applyDesktopTeammateModeDecisionToEnv(shellEnv, teammateModeDecision);
|
||||
let mcpConfigPath: string;
|
||||
let bootstrapSpecPath: string;
|
||||
let bootstrapUserPromptPath: string | null = null;
|
||||
|
|
@ -12945,6 +12950,7 @@ export class TeamProvisioningService {
|
|||
if (request.worktree) {
|
||||
launchArgs.push('--worktree', request.worktree);
|
||||
}
|
||||
launchArgs.push(...buildDesktopTeammateModeCliArgs(teammateModeDecision));
|
||||
launchArgs.push(...parseCliArgs(request.extraCliArgs));
|
||||
launchArgs.push(...providerArgs);
|
||||
// When the lead uses a different provider than some teammates (e.g., anthropic lead
|
||||
|
|
@ -15500,8 +15506,9 @@ export class TeamProvisioningService {
|
|||
}
|
||||
|
||||
const paneIds = [...metadataByMember.values()]
|
||||
.filter((metadata) => metadata.backendType === 'tmux' || metadata.backendType === undefined)
|
||||
.map((metadata) => metadata.tmuxPaneId?.trim() ?? '')
|
||||
.filter((paneId) => paneId.length > 0);
|
||||
.filter((paneId) => paneId.length > 0 && !paneId.startsWith('process:'));
|
||||
let paneInfoById = new Map<string, TmuxPaneRuntimeInfo>();
|
||||
if (paneIds.length > 0) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -58,22 +58,42 @@ export async function resolveDesktopTeammateModeDecision(
|
|||
};
|
||||
}
|
||||
|
||||
if (explicitMode === 'auto' || explicitMode === 'in-process') {
|
||||
if (explicitMode === 'auto') {
|
||||
return {
|
||||
injectedTeammateMode: null,
|
||||
forceProcessTeammates: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (explicitMode === 'in-process') {
|
||||
return {
|
||||
injectedTeammateMode: null,
|
||||
forceProcessTeammates: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!(await isTmuxAvailable())) {
|
||||
return {
|
||||
injectedTeammateMode: null,
|
||||
forceProcessTeammates: false,
|
||||
};
|
||||
}
|
||||
const tmuxAvailable = await isTmuxAvailable();
|
||||
|
||||
return {
|
||||
injectedTeammateMode: 'tmux',
|
||||
injectedTeammateMode: tmuxAvailable ? 'tmux' : null,
|
||||
forceProcessTeammates: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyDesktopTeammateModeDecisionToEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
decision: Pick<DesktopTeammateModeDecision, 'forceProcessTeammates'>
|
||||
): void {
|
||||
if (decision.forceProcessTeammates) {
|
||||
env.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES = '1';
|
||||
return;
|
||||
}
|
||||
|
||||
delete env.CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES;
|
||||
}
|
||||
|
||||
export function buildDesktopTeammateModeCliArgs(
|
||||
decision: Pick<DesktopTeammateModeDecision, 'injectedTeammateMode'>
|
||||
): string[] {
|
||||
return decision.injectedTeammateMode ? ['--teammate-mode', decision.injectedTeammateMode] : [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type TeammateRuntimeIssueReason =
|
|||
| 'mixed-provider'
|
||||
| 'codex-native-runtime'
|
||||
| 'explicit-tmux-mode'
|
||||
| 'explicit-in-process-mode'
|
||||
| 'opencode-led-mixed-unsupported';
|
||||
|
||||
interface RuntimeMemberInput {
|
||||
|
|
@ -176,6 +177,13 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
}
|
||||
}
|
||||
|
||||
const requiresSeparateProcess = issues.some(
|
||||
(issue) => issue.reason === 'mixed-provider' || issue.reason === 'codex-native-runtime'
|
||||
);
|
||||
if (explicitTeammateMode === 'in-process' && requiresSeparateProcess) {
|
||||
issues.push({ reason: 'explicit-in-process-mode' });
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
return {
|
||||
visible: false,
|
||||
|
|
@ -193,7 +201,9 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
const hasOpenCodeLeadMixedUnsupported = issues.some(
|
||||
(issue) => issue.reason === 'opencode-led-mixed-unsupported'
|
||||
);
|
||||
if (tmuxReady && !hasOpenCodeLeadMixedUnsupported) {
|
||||
const hasExplicitTmux = issues.some((issue) => issue.reason === 'explicit-tmux-mode');
|
||||
const hasExplicitInProcess = issues.some((issue) => issue.reason === 'explicit-in-process-mode');
|
||||
if (!hasOpenCodeLeadMixedUnsupported && !hasExplicitTmux && !hasExplicitInProcess) {
|
||||
return {
|
||||
visible: false,
|
||||
blocksSubmission: false,
|
||||
|
|
@ -206,11 +216,28 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
};
|
||||
}
|
||||
|
||||
const checking = !hasOpenCodeLeadMixedUnsupported && tmuxStatusLoading && !tmuxStatus;
|
||||
if (tmuxReady && hasExplicitTmux && !hasOpenCodeLeadMixedUnsupported && !hasExplicitInProcess) {
|
||||
return {
|
||||
visible: false,
|
||||
blocksSubmission: false,
|
||||
checking: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: [],
|
||||
tmuxDetail: null,
|
||||
memberWarningById: {},
|
||||
};
|
||||
}
|
||||
|
||||
const checking =
|
||||
hasExplicitTmux &&
|
||||
!hasOpenCodeLeadMixedUnsupported &&
|
||||
!hasExplicitInProcess &&
|
||||
tmuxStatusLoading &&
|
||||
!tmuxStatus;
|
||||
const blocksSubmission = true;
|
||||
const hasMixedProviders = issues.some((issue) => issue.reason === 'mixed-provider');
|
||||
const hasCodexNative = issues.some((issue) => issue.reason === 'codex-native-runtime');
|
||||
const hasExplicitTmux = issues.some((issue) => issue.reason === 'explicit-tmux-mode');
|
||||
const details: string[] = [];
|
||||
const memberWarningById: Record<string, string> = {};
|
||||
|
||||
|
|
@ -241,15 +268,20 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
if (hasExplicitTmux) {
|
||||
details.push('Custom CLI args force --teammate-mode tmux.');
|
||||
}
|
||||
if (hasExplicitInProcess) {
|
||||
details.push('Custom CLI args force --teammate-mode in-process.');
|
||||
}
|
||||
if (hasOpenCodeLeadMixedUnsupported) {
|
||||
details.push(
|
||||
'Fix: keep the team lead on Anthropic, Codex, or Gemini when mixing OpenCode with other providers.'
|
||||
);
|
||||
} else if (hasExplicitInProcess) {
|
||||
details.push(
|
||||
'Fix: remove --teammate-mode in-process so teammates can use native process transport.'
|
||||
);
|
||||
} else {
|
||||
details.push(
|
||||
hasCodexNative && !hasMixedProviders
|
||||
? 'Fix: install tmux/WSL tmux, use Solo team, or choose a same-provider runtime that supports in-process teammates.'
|
||||
: 'Fix: install tmux/WSL tmux, use Solo team, or keep every teammate on the same non-Codex-native provider as the lead.'
|
||||
'Fix: install tmux/WSL tmux, or remove --teammate-mode tmux so the app can use native process transport.'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -260,10 +292,10 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
if (issue.reason === 'mixed-provider') {
|
||||
memberWarningById[issue.memberId] =
|
||||
`${issue.memberName} uses ${getProviderLabel(issue.memberProviderId ?? leadProviderId)}. ` +
|
||||
`Without tmux, teammates must use the same provider as the ${getProviderLabel(leadProviderId)} lead.`;
|
||||
`This teammate requires a separate process outside the ${getProviderLabel(leadProviderId)} lead.`;
|
||||
} else if (issue.reason === 'codex-native-runtime') {
|
||||
memberWarningById[issue.memberId] =
|
||||
`${issue.memberName} uses Codex native. Codex native teammates require a separate process, which currently needs tmux.`;
|
||||
`${issue.memberName} uses Codex native. Codex native teammates require a separate Codex process.`;
|
||||
} else if (issue.reason === 'opencode-led-mixed-unsupported') {
|
||||
memberWarningById[issue.memberId] =
|
||||
`${issue.memberName} uses ${getProviderLabel(issue.memberProviderId ?? leadProviderId)}. ` +
|
||||
|
|
@ -276,19 +308,19 @@ export function analyzeTeammateRuntimeCompatibility({
|
|||
blocksSubmission,
|
||||
checking,
|
||||
title: checking
|
||||
? 'Checking tmux runtime for teammate support'
|
||||
? 'Checking tmux runtime for explicit teammate mode'
|
||||
: hasOpenCodeLeadMixedUnsupported
|
||||
? 'OpenCode cannot lead mixed-provider teams'
|
||||
: hasCodexNative && !hasMixedProviders
|
||||
? 'Codex teammates need tmux before they can run'
|
||||
: 'This team needs tmux before it can run',
|
||||
: hasExplicitInProcess
|
||||
? 'This team cannot use in-process teammates'
|
||||
: 'tmux is not ready for explicit teammate mode',
|
||||
message: checking
|
||||
? 'Some teammates require separate processes. The app is checking whether tmux is available.'
|
||||
? 'Custom CLI args request tmux teammates. The app is checking whether tmux is available.'
|
||||
: hasOpenCodeLeadMixedUnsupported
|
||||
? 'OpenCode teammates can run as secondary runtime lanes under an Anthropic, Codex, or Gemini lead, but OpenCode-led mixed teams are not supported in this phase.'
|
||||
: hasCodexNative && !hasMixedProviders
|
||||
? 'The Codex lead can run without tmux, but Codex native teammates cannot use the in-process teammate adapter. They must start as separate Codex processes, and this path currently needs tmux.'
|
||||
: 'tmux is not ready on this machine. Same-provider in-process teammates can run without tmux, but this team has teammates that require separate processes.',
|
||||
: hasExplicitInProcess
|
||||
? 'Some teammates require separate processes. Remove --teammate-mode in-process so the app can use native process transport.'
|
||||
: 'Custom CLI args force --teammate-mode tmux, but tmux is not ready. Remove that arg to use native process transport on Windows, or install tmux/WSL tmux.',
|
||||
details,
|
||||
tmuxDetail: getTmuxDetail(tmuxStatus, tmuxStatusError),
|
||||
memberWarningById,
|
||||
|
|
|
|||
|
|
@ -604,6 +604,41 @@ describe('TeamProvisioningService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not send legacy process backend pane markers to tmux liveness lookup', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
getConfig: vi.fn(async () => ({
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'alice', model: 'gpt-5.4-mini' },
|
||||
],
|
||||
})),
|
||||
};
|
||||
(svc as any).readPersistedRuntimeMembers = vi.fn(() => [
|
||||
{
|
||||
name: 'alice',
|
||||
agentId: 'alice@runtime-team',
|
||||
tmuxPaneId: 'process:4242',
|
||||
},
|
||||
]);
|
||||
(svc as any).aliveRunByTeam.set('runtime-team', 'run-1');
|
||||
(svc as any).runs.set('run-1', {
|
||||
runId: 'run-1',
|
||||
child: { pid: 111 },
|
||||
request: { model: 'gpt-5.4' },
|
||||
processKilled: false,
|
||||
cancelRequested: false,
|
||||
spawnContext: null,
|
||||
});
|
||||
vi.mocked(pidusage).mockResolvedValueOnce({
|
||||
'111': createPidusageStat(111, 123_000_000),
|
||||
} as any);
|
||||
|
||||
await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||
|
||||
expect(listTmuxPaneRuntimeInfoForCurrentPlatform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('exposes providerBackendId from the live run request when available', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).configReader = {
|
||||
|
|
|
|||
|
|
@ -23,15 +23,65 @@ describe('runtimeTeammateMode', () => {
|
|||
expect(decision.injectedTeammateMode).toBe('tmux');
|
||||
});
|
||||
|
||||
it('keeps fallback mode when tmux runtime is not ready', async () => {
|
||||
it('uses native process teammates when tmux runtime is not ready', async () => {
|
||||
mockIsTmuxRuntimeReadyForCurrentPlatform.mockResolvedValue(false);
|
||||
const { resolveDesktopTeammateModeDecision } =
|
||||
await import('@main/services/team/runtimeTeammateMode');
|
||||
|
||||
const decision = await resolveDesktopTeammateModeDecision(undefined);
|
||||
|
||||
expect(decision.forceProcessTeammates).toBe(true);
|
||||
expect(decision.injectedTeammateMode).toBeNull();
|
||||
});
|
||||
|
||||
it('treats explicit auto mode as automatic process teammate selection without injection', async () => {
|
||||
mockIsTmuxRuntimeReadyForCurrentPlatform.mockResolvedValue(true);
|
||||
const { resolveDesktopTeammateModeDecision } =
|
||||
await import('@main/services/team/runtimeTeammateMode');
|
||||
|
||||
const decision = await resolveDesktopTeammateModeDecision('--teammate-mode auto');
|
||||
const equalsDecision = await resolveDesktopTeammateModeDecision('--teammate-mode=auto');
|
||||
|
||||
expect(decision.forceProcessTeammates).toBe(true);
|
||||
expect(decision.injectedTeammateMode).toBeNull();
|
||||
expect(equalsDecision.forceProcessTeammates).toBe(true);
|
||||
expect(equalsDecision.injectedTeammateMode).toBeNull();
|
||||
expect(mockIsTmuxRuntimeReadyForCurrentPlatform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('honors explicit in-process mode as an opt-out from process teammates', async () => {
|
||||
mockIsTmuxRuntimeReadyForCurrentPlatform.mockResolvedValue(true);
|
||||
const { resolveDesktopTeammateModeDecision } =
|
||||
await import('@main/services/team/runtimeTeammateMode');
|
||||
|
||||
const decision = await resolveDesktopTeammateModeDecision('--teammate-mode=in-process');
|
||||
|
||||
expect(decision.forceProcessTeammates).toBe(false);
|
||||
expect(decision.injectedTeammateMode).toBeNull();
|
||||
expect(mockIsTmuxRuntimeReadyForCurrentPlatform).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes inherited process fallback env when explicit in-process mode opts out', async () => {
|
||||
const { applyDesktopTeammateModeDecisionToEnv } =
|
||||
await import('@main/services/team/runtimeTeammateMode');
|
||||
const env = {
|
||||
CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES: '1',
|
||||
};
|
||||
|
||||
applyDesktopTeammateModeDecisionToEnv(env, { forceProcessTeammates: false });
|
||||
|
||||
expect(env).not.toHaveProperty('CLAUDE_TEAM_FORCE_PROCESS_TEAMMATES');
|
||||
});
|
||||
|
||||
it('builds injected teammate mode cli args only when a mode was selected', async () => {
|
||||
const { buildDesktopTeammateModeCliArgs } =
|
||||
await import('@main/services/team/runtimeTeammateMode');
|
||||
|
||||
expect(buildDesktopTeammateModeCliArgs({ injectedTeammateMode: 'tmux' })).toEqual([
|
||||
'--teammate-mode',
|
||||
'tmux',
|
||||
]);
|
||||
expect(buildDesktopTeammateModeCliArgs({ injectedTeammateMode: null })).toEqual([]);
|
||||
});
|
||||
|
||||
it('re-checks tmux readiness after the environment changes instead of keeping a stale negative cache', async () => {
|
||||
|
|
@ -44,7 +94,7 @@ describe('runtimeTeammateMode', () => {
|
|||
const firstDecision = await resolveDesktopTeammateModeDecision(undefined);
|
||||
const secondDecision = await resolveDesktopTeammateModeDecision(undefined);
|
||||
|
||||
expect(firstDecision.forceProcessTeammates).toBe(false);
|
||||
expect(firstDecision.forceProcessTeammates).toBe(true);
|
||||
expect(firstDecision.injectedTeammateMode).toBeNull();
|
||||
expect(secondDecision.forceProcessTeammates).toBe(true);
|
||||
expect(secondDecision.injectedTeammateMode).toBe('tmux');
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ async function flushReact(): Promise<void> {
|
|||
await Promise.resolve();
|
||||
}
|
||||
|
||||
function withoutNumberGroupSeparators(value: string | null | undefined): string {
|
||||
return (value ?? '').replace(/[\s,\u00a0\u202f]/g, '');
|
||||
}
|
||||
|
||||
describe('TokenUsageDisplay', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
|
|
@ -90,7 +94,7 @@ describe('TokenUsageDisplay', () => {
|
|||
|
||||
const popover = document.querySelector('[role="tooltip"]');
|
||||
expect(popover).toBeTruthy();
|
||||
expect(popover?.textContent).toContain('2,250');
|
||||
expect(withoutNumberGroupSeparators(popover?.textContent)).toContain('2250');
|
||||
expect(popover?.textContent).toContain('500 (25.0% of prompt input)');
|
||||
expect(popover?.textContent).not.toContain('of context');
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe('analyzeTeammateRuntimeCompatibility', () => {
|
|||
expect(result.memberWarningById).toEqual({});
|
||||
});
|
||||
|
||||
it('blocks mixed-provider teammates when tmux is unavailable', () => {
|
||||
it('allows mixed-provider teammates through native process transport when tmux is unavailable', () => {
|
||||
const result = analyzeTeammateRuntimeCompatibility({
|
||||
leadProviderId: 'anthropic',
|
||||
members: [{ id: 'bob', name: 'bob', providerId: 'codex' }],
|
||||
|
|
@ -64,9 +64,9 @@ describe('analyzeTeammateRuntimeCompatibility', () => {
|
|||
tmuxStatusError: null,
|
||||
});
|
||||
|
||||
expect(result.blocksSubmission).toBe(true);
|
||||
expect(result.details.join('\n')).toContain('Mixed providers');
|
||||
expect(result.memberWarningById.bob).toContain('same provider as the Anthropic lead');
|
||||
expect(result.blocksSubmission).toBe(false);
|
||||
expect(result.visible).toBe(false);
|
||||
expect(result.memberWarningById).toEqual({});
|
||||
});
|
||||
|
||||
it('allows OpenCode secondary-lane teammates without tmux under a non-OpenCode lead', () => {
|
||||
|
|
@ -98,7 +98,7 @@ describe('analyzeTeammateRuntimeCompatibility', () => {
|
|||
expect(result.memberWarningById.bob).toContain('OpenCode cannot be the team lead');
|
||||
});
|
||||
|
||||
it('blocks same-provider Codex native teammates when tmux is unavailable', () => {
|
||||
it('allows same-provider Codex native teammates through native process transport when tmux is unavailable', () => {
|
||||
const result = analyzeTeammateRuntimeCompatibility({
|
||||
leadProviderId: 'codex',
|
||||
leadProviderBackendId: 'codex-native',
|
||||
|
|
@ -108,11 +108,9 @@ describe('analyzeTeammateRuntimeCompatibility', () => {
|
|||
tmuxStatusError: null,
|
||||
});
|
||||
|
||||
expect(result.blocksSubmission).toBe(true);
|
||||
expect(result.title).toBe('Codex teammates need tmux before they can run');
|
||||
expect(result.message).toContain('The Codex lead can run without tmux');
|
||||
expect(result.details.join('\n')).toContain('Codex native teammates');
|
||||
expect(result.memberWarningById.jack).toContain('Codex native teammates require');
|
||||
expect(result.blocksSubmission).toBe(false);
|
||||
expect(result.visible).toBe(false);
|
||||
expect(result.memberWarningById).toEqual({});
|
||||
});
|
||||
|
||||
it('allows separate-process teammate requirements when tmux is ready', () => {
|
||||
|
|
@ -155,5 +153,23 @@ describe('analyzeTeammateRuntimeCompatibility', () => {
|
|||
|
||||
expect(result.blocksSubmission).toBe(true);
|
||||
expect(result.details).toContain('Custom CLI args force --teammate-mode tmux.');
|
||||
expect(result.message).toContain('native process transport');
|
||||
});
|
||||
|
||||
it('blocks explicit in-process mode when a teammate requires a separate process', () => {
|
||||
const result = analyzeTeammateRuntimeCompatibility({
|
||||
leadProviderId: 'anthropic',
|
||||
members: [{ id: 'bob', name: 'bob', providerId: 'codex' }],
|
||||
extraCliArgs: '--teammate-mode=in-process',
|
||||
tmuxStatus: buildTmuxStatus(true),
|
||||
tmuxStatusLoading: false,
|
||||
tmuxStatusError: null,
|
||||
});
|
||||
|
||||
expect(result.blocksSubmission).toBe(true);
|
||||
expect(result.title).toBe('This team cannot use in-process teammates');
|
||||
expect(result.details).toContain('Custom CLI args force --teammate-mode in-process.');
|
||||
expect(result.message).toContain('native process transport');
|
||||
expect(result.memberWarningById.bob).toContain('requires a separate process');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue