feat(team): make tmux optional for desktop teammates
This commit is contained in:
parent
afe50439b1
commit
31ae128778
7 changed files with 213 additions and 49 deletions
|
|
@ -121,7 +121,6 @@ import {
|
|||
} from '../runtime/providerModelProbe';
|
||||
import { resolveTeamProviderId } from '../runtime/providerRuntimeEnv';
|
||||
|
||||
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
|
||||
import {
|
||||
createOpenCodePromptDeliveryLedgerStore,
|
||||
hashOpenCodePromptDeliveryPayload,
|
||||
|
|
@ -133,16 +132,17 @@ import {
|
|||
} from './opencode/delivery/OpenCodePromptDeliveryLedger';
|
||||
import {
|
||||
isOpenCodePromptDeliveryObserveLaterResponseState,
|
||||
isOpenCodePromptDeliveryRetryAttemptDue,
|
||||
isOpenCodePromptDeliveryRetryableResponseState,
|
||||
isOpenCodeVisibleReplySemanticallySufficient,
|
||||
isOpenCodePromptDeliveryRetryAttemptDue,
|
||||
isOpenCodeVisibleReplyReadCommitAllowed,
|
||||
isOpenCodeVisibleReplySemanticallySufficient,
|
||||
OPENCODE_PROMPT_DELIVERY_OBSERVE_DELAY_MS,
|
||||
OPENCODE_PROMPT_DELIVERY_RETRY_DELAY_MS,
|
||||
OPENCODE_PROMPT_WATCHDOG_GLOBAL_CONCURRENCY,
|
||||
OPENCODE_PROMPT_WATCHDOG_PER_TEAM_CONCURRENCY,
|
||||
type OpenCodeVisibleReplyProof,
|
||||
} from './opencode/delivery/OpenCodePromptDeliveryWatchdog';
|
||||
import { createRuntimeDeliveryJournalStore } from './opencode/delivery/RuntimeDeliveryJournal';
|
||||
import {
|
||||
type RuntimeDeliveryDestinationPort,
|
||||
RuntimeDeliveryDestinationRegistry,
|
||||
|
|
@ -184,7 +184,11 @@ import {
|
|||
buildProgressAssistantOutput,
|
||||
buildProgressLogsTail,
|
||||
} from './progressPayload';
|
||||
import { resolveDesktopTeammateModeDecision } from './runtimeTeammateMode';
|
||||
import {
|
||||
applyDesktopTeammateModeDecisionToEnv,
|
||||
buildDesktopTeammateModeCliArgs,
|
||||
resolveDesktopTeammateModeDecision,
|
||||
} from './runtimeTeammateMode';
|
||||
import {
|
||||
choosePreferredLaunchSnapshot,
|
||||
clearBootstrapState,
|
||||
|
|
@ -205,8 +209,8 @@ import {
|
|||
import { TeamLaunchStateStore } from './TeamLaunchStateStore';
|
||||
import { TeamMcpConfigBuilder } from './TeamMcpConfigBuilder';
|
||||
import { TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
||||
import { TeamMemberWorktreeManager } from './TeamMemberWorktreeManager';
|
||||
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
|
||||
import { TeamMemberWorktreeManager } from './TeamMemberWorktreeManager';
|
||||
import { TeamMetaStore } from './TeamMetaStore';
|
||||
import {
|
||||
commandArgEquals,
|
||||
|
|
@ -1099,6 +1103,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;
|
||||
|
|
@ -1162,6 +1169,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,
|
||||
},
|
||||
|
|
@ -11744,9 +11752,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;
|
||||
|
|
@ -11810,6 +11816,7 @@ export class TeamProvisioningService {
|
|||
...(launchIdentity.resolvedEffort ? ['--effort', launchIdentity.resolvedEffort] : []),
|
||||
...providerFastModeArgs,
|
||||
...(request.worktree ? ['--worktree', request.worktree] : []),
|
||||
...buildDesktopTeammateModeCliArgs(teammateModeDecision),
|
||||
...parseCliArgs(request.extraCliArgs),
|
||||
...providerArgs,
|
||||
];
|
||||
|
|
@ -12812,9 +12819,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;
|
||||
|
|
@ -12894,6 +12899,7 @@ export class TeamProvisioningService {
|
|||
if (request.worktree) {
|
||||
launchArgs.push('--worktree', request.worktree);
|
||||
}
|
||||
launchArgs.push(...buildDesktopTeammateModeCliArgs(teammateModeDecision));
|
||||
launchArgs.push(...parseCliArgs(request.extraCliArgs));
|
||||
launchArgs.push(...providerArgs);
|
||||
const runtimeWarning = buildRuntimeLaunchWarning(request, shellEnv, {
|
||||
|
|
@ -15438,8 +15444,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,
|
||||
|
|
|
|||
|
|
@ -603,6 +603,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