fix(codex): retry binary discovery after shell env loads
This commit is contained in:
parent
554112a3d8
commit
9cd5144e1a
6 changed files with 165 additions and 23 deletions
|
|
@ -1,27 +1,27 @@
|
||||||
{
|
{
|
||||||
"version": "0.0.38",
|
"version": "0.0.39",
|
||||||
"sourceRef": "v0.0.38",
|
"sourceRef": "v0.0.39",
|
||||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||||
"releaseRepository": "777genius/agent-teams-ai",
|
"releaseRepository": "777genius/agent-teams-ai",
|
||||||
"releaseTag": "v2.0.0",
|
"releaseTag": "v2.0.0",
|
||||||
"assets": {
|
"assets": {
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.38.tar.gz",
|
"file": "agent-teams-runtime-darwin-arm64-v0.0.39.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"darwin-x64": {
|
"darwin-x64": {
|
||||||
"file": "agent-teams-runtime-darwin-x64-v0.0.38.tar.gz",
|
"file": "agent-teams-runtime-darwin-x64-v0.0.39.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"linux-x64": {
|
"linux-x64": {
|
||||||
"file": "agent-teams-runtime-linux-x64-v0.0.38.tar.gz",
|
"file": "agent-teams-runtime-linux-x64-v0.0.39.tar.gz",
|
||||||
"archiveKind": "tar.gz",
|
"archiveKind": "tar.gz",
|
||||||
"binaryName": "claude-multimodel"
|
"binaryName": "claude-multimodel"
|
||||||
},
|
},
|
||||||
"win32-x64": {
|
"win32-x64": {
|
||||||
"file": "agent-teams-runtime-win32-x64-v0.0.38.zip",
|
"file": "agent-teams-runtime-win32-x64-v0.0.39.zip",
|
||||||
"archiveKind": "zip",
|
"archiveKind": "zip",
|
||||||
"binaryName": "claude-multimodel.exe"
|
"binaryName": "claude-multimodel.exe"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
CodexBinaryResolver,
|
CodexBinaryResolver,
|
||||||
JsonRpcStdioClient,
|
JsonRpcStdioClient,
|
||||||
} from '@main/services/infrastructure/codexAppServer';
|
} from '@main/services/infrastructure/codexAppServer';
|
||||||
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
import { getCachedShellEnv, resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv';
|
||||||
|
|
||||||
import { CodexAccountSnapshotPresenter } from '../adapters/output/presenters/CodexAccountSnapshotPresenter';
|
import { CodexAccountSnapshotPresenter } from '../adapters/output/presenters/CodexAccountSnapshotPresenter';
|
||||||
import { CodexAccountAppServerClient } from '../infrastructure/CodexAccountAppServerClient';
|
import { CodexAccountAppServerClient } from '../infrastructure/CodexAccountAppServerClient';
|
||||||
|
|
@ -41,6 +41,7 @@ type LoggerPort = Pick<Logger, 'info' | 'warn' | 'error'>;
|
||||||
const SNAPSHOT_CACHE_TTL_MS = 5_000;
|
const SNAPSHOT_CACHE_TTL_MS = 5_000;
|
||||||
const RATE_LIMITS_CACHE_TTL_MS = 45_000;
|
const RATE_LIMITS_CACHE_TTL_MS = 45_000;
|
||||||
const LAST_KNOWN_GOOD_MANAGED_ACCOUNT_TTL_MS = 60_000;
|
const LAST_KNOWN_GOOD_MANAGED_ACCOUNT_TTL_MS = 60_000;
|
||||||
|
const CODEX_BINARY_COLD_RETRY_TIMEOUT_MS = 12_000;
|
||||||
|
|
||||||
interface CodexLastKnownAccount {
|
interface CodexLastKnownAccount {
|
||||||
payload: CodexAppServerGetAccountResponse;
|
payload: CodexAppServerGetAccountResponse;
|
||||||
|
|
@ -251,6 +252,20 @@ function createDeferred(): { promise: Promise<void>; resolve: () => void } {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveCodexBinaryForAccountSnapshot(): Promise<string | null> {
|
||||||
|
const binaryPath = await CodexBinaryResolver.resolve();
|
||||||
|
if (binaryPath) {
|
||||||
|
return binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
await resolveInteractiveShellEnvBestEffort({
|
||||||
|
timeoutMs: CODEX_BINARY_COLD_RETRY_TIMEOUT_MS,
|
||||||
|
fallbackEnv: process.env,
|
||||||
|
});
|
||||||
|
CodexBinaryResolver.clearCache();
|
||||||
|
return CodexBinaryResolver.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
export interface CodexAccountFeatureFacade {
|
export interface CodexAccountFeatureFacade {
|
||||||
getSnapshot(): Promise<CodexAccountSnapshotDto>;
|
getSnapshot(): Promise<CodexAccountSnapshotDto>;
|
||||||
refreshSnapshot(options?: {
|
refreshSnapshot(options?: {
|
||||||
|
|
@ -351,7 +366,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
}): Promise<CodexAccountSnapshotDto> {
|
}): Promise<CodexAccountSnapshotDto> {
|
||||||
let binaryMissing = false;
|
let binaryMissing = false;
|
||||||
await this.runSerializedMutation(async () => {
|
await this.runSerializedMutation(async () => {
|
||||||
const binaryPath = await CodexBinaryResolver.resolve();
|
const binaryPath = await resolveCodexBinaryForAccountSnapshot();
|
||||||
if (!binaryPath) {
|
if (!binaryPath) {
|
||||||
binaryMissing = true;
|
binaryMissing = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -380,7 +395,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
await this.runSerializedMutation(async () => {
|
await this.runSerializedMutation(async () => {
|
||||||
await this.loginSessionManager.cancel().catch(() => undefined);
|
await this.loginSessionManager.cancel().catch(() => undefined);
|
||||||
|
|
||||||
const binaryPath = await CodexBinaryResolver.resolve();
|
const binaryPath = await resolveCodexBinaryForAccountSnapshot();
|
||||||
if (!binaryPath) {
|
if (!binaryPath) {
|
||||||
throw new Error('Codex CLI is not available, so logout cannot be completed.');
|
throw new Error('Codex CLI is not available, so logout cannot be completed.');
|
||||||
}
|
}
|
||||||
|
|
@ -467,7 +482,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
||||||
const localAccountState = await detectCodexLocalAccountState();
|
const localAccountState = await detectCodexLocalAccountState();
|
||||||
const localAccountArtifactsPresent = localAccountState.hasArtifacts;
|
const localAccountArtifactsPresent = localAccountState.hasArtifacts;
|
||||||
const localActiveChatgptAccountPresent = localAccountState.hasActiveChatgptAccount;
|
const localActiveChatgptAccountPresent = localAccountState.hasActiveChatgptAccount;
|
||||||
const binaryPath = await CodexBinaryResolver.resolve();
|
const binaryPath = await resolveCodexBinaryForAccountSnapshot();
|
||||||
const login = this.loginSessionManager.getState();
|
const login = this.loginSessionManager.getState();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const BINARY_LAUNCH_VERIFY_TIMEOUT_MS = 3_000;
|
||||||
let cachedBinaryPath: string | null | undefined;
|
let cachedBinaryPath: string | null | undefined;
|
||||||
let cacheVerifiedAt = 0;
|
let cacheVerifiedAt = 0;
|
||||||
let resolveInFlight: Promise<string | null> | null = null;
|
let resolveInFlight: Promise<string | null> | null = null;
|
||||||
|
let cachedMissHadShellEnv = false;
|
||||||
const versionCache = new Map<string, { version: string | null; observedAt: number }>();
|
const versionCache = new Map<string, { version: string | null; observedAt: number }>();
|
||||||
|
|
||||||
async function fileExists(filePath: string): Promise<boolean> {
|
async function fileExists(filePath: string): Promise<boolean> {
|
||||||
|
|
@ -121,24 +122,33 @@ export class CodexBinaryResolver {
|
||||||
cachedBinaryPath = undefined;
|
cachedBinaryPath = undefined;
|
||||||
cacheVerifiedAt = 0;
|
cacheVerifiedAt = 0;
|
||||||
resolveInFlight = null;
|
resolveInFlight = null;
|
||||||
|
cachedMissHadShellEnv = false;
|
||||||
versionCache.clear();
|
versionCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resolve(): Promise<string | null> {
|
static async resolve(): Promise<string | null> {
|
||||||
if (cachedBinaryPath !== undefined) {
|
if (cachedBinaryPath !== undefined) {
|
||||||
if (cachedBinaryPath === null) {
|
if (cachedBinaryPath === null) {
|
||||||
const verifiedAppManagedBinaryPath =
|
if (!cachedMissHadShellEnv && getCachedShellEnv() !== null) {
|
||||||
await resolveVerifiedAppManagedCodexRuntimeBinaryPath();
|
cachedBinaryPath = undefined;
|
||||||
if (verifiedAppManagedBinaryPath) {
|
cacheVerifiedAt = 0;
|
||||||
cachedBinaryPath = verifiedAppManagedBinaryPath;
|
cachedMissHadShellEnv = false;
|
||||||
cacheVerifiedAt = Date.now();
|
} else {
|
||||||
return verifiedAppManagedBinaryPath;
|
const verifiedAppManagedBinaryPath =
|
||||||
|
await resolveVerifiedAppManagedCodexRuntimeBinaryPath();
|
||||||
|
if (verifiedAppManagedBinaryPath) {
|
||||||
|
cachedBinaryPath = verifiedAppManagedBinaryPath;
|
||||||
|
cacheVerifiedAt = Date.now();
|
||||||
|
cachedMissHadShellEnv = false;
|
||||||
|
return verifiedAppManagedBinaryPath;
|
||||||
|
}
|
||||||
|
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
cachedBinaryPath = undefined;
|
||||||
|
cacheVerifiedAt = 0;
|
||||||
|
cachedMissHadShellEnv = false;
|
||||||
}
|
}
|
||||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
cachedBinaryPath = undefined;
|
|
||||||
cacheVerifiedAt = 0;
|
|
||||||
} else {
|
} else {
|
||||||
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
if (Date.now() - cacheVerifiedAt <= CACHE_VERIFY_TTL_MS) {
|
||||||
return cachedBinaryPath;
|
return cachedBinaryPath;
|
||||||
|
|
@ -147,6 +157,7 @@ export class CodexBinaryResolver {
|
||||||
const verified = await verifyBinary(cachedBinaryPath);
|
const verified = await verifyBinary(cachedBinaryPath);
|
||||||
if (verified) {
|
if (verified) {
|
||||||
cacheVerifiedAt = Date.now();
|
cacheVerifiedAt = Date.now();
|
||||||
|
cachedMissHadShellEnv = false;
|
||||||
return verified;
|
return verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,12 +189,14 @@ export class CodexBinaryResolver {
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
cachedBinaryPath = resolved;
|
cachedBinaryPath = resolved;
|
||||||
cacheVerifiedAt = Date.now();
|
cacheVerifiedAt = Date.now();
|
||||||
|
cachedMissHadShellEnv = false;
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedBinaryPath = null;
|
cachedBinaryPath = null;
|
||||||
cacheVerifiedAt = Date.now();
|
cacheVerifiedAt = Date.now();
|
||||||
|
cachedMissHadShellEnv = getCachedShellEnv() !== null;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,34 @@ describe('CodexBinaryResolver', () => {
|
||||||
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recovers a cold negative cache entry as soon as shell env becomes available', async () => {
|
||||||
|
setPlatform('darwin');
|
||||||
|
process.env.PATH = '/usr/bin:/bin:/usr/sbin:/sbin';
|
||||||
|
const shellPath = '/usr/local/bin:/usr/bin:/bin';
|
||||||
|
const codexShim = path.posix.join('/usr/local/bin', 'codex');
|
||||||
|
buildMergedCliPathMock.mockReturnValue('/usr/bin:/bin:/usr/sbin:/sbin');
|
||||||
|
|
||||||
|
accessMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||||
|
|
||||||
|
const { CodexBinaryResolver } = await import('../CodexBinaryResolver');
|
||||||
|
CodexBinaryResolver.clearCache();
|
||||||
|
|
||||||
|
await expect(CodexBinaryResolver.resolve()).resolves.toBeNull();
|
||||||
|
|
||||||
|
getCachedShellEnvMock.mockReturnValue({
|
||||||
|
HOME: '/Users/tester',
|
||||||
|
PATH: shellPath,
|
||||||
|
});
|
||||||
|
accessMock.mockImplementation((filePath) => {
|
||||||
|
if (filePath === codexShim) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(CodexBinaryResolver.resolve()).resolves.toBe(codexShim);
|
||||||
|
});
|
||||||
|
|
||||||
it('skips Windows PATH candidates that exist but cannot be launched', async () => {
|
it('skips Windows PATH candidates that exist but cannot be launched', async () => {
|
||||||
const blockedDir =
|
const blockedDir =
|
||||||
'C:\\Program Files\\WindowsApps\\OpenAI.Codex_26.422.3464.0_x64__2p2nqsd0c76g0\\app\\resources';
|
'C:\\Program Files\\WindowsApps\\OpenAI.Codex_26.422.3464.0_x64__2p2nqsd0c76g0\\app\\resources';
|
||||||
|
|
|
||||||
|
|
@ -20061,14 +20061,28 @@ export class TeamProvisioningService {
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const { stdout } = await execCli(
|
const { stdout } = await execCli(
|
||||||
claudePath,
|
claudePath,
|
||||||
buildProviderCliCommandArgs(providerArgs, ['model', 'list', '--json', '--provider', 'all']),
|
buildProviderCliCommandArgs(providerArgs, [
|
||||||
|
'model',
|
||||||
|
'list',
|
||||||
|
'--json',
|
||||||
|
'--provider',
|
||||||
|
providerId,
|
||||||
|
]),
|
||||||
{
|
{
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
env,
|
||||||
timeout: 10_000,
|
timeout: 10_000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(stdout);
|
let parsed: ProviderModelListCommandResponse;
|
||||||
|
try {
|
||||||
|
parsed = extractJsonObjectFromCli<ProviderModelListCommandResponse>(stdout);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse runtime default model list for ${getTeamProviderLabel(providerId)} (${providerId}): ${message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
const defaultModel = parsed.providers?.[providerId]?.defaultModel;
|
const defaultModel = parsed.providers?.[providerId]?.defaultModel;
|
||||||
const normalizedDefaultModel =
|
const normalizedDefaultModel =
|
||||||
typeof defaultModel === 'string' && defaultModel.trim().length > 0
|
typeof defaultModel === 'string' && defaultModel.trim().length > 0
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
const {
|
const {
|
||||||
apiKeyHasPreferredMock,
|
apiKeyHasPreferredMock,
|
||||||
apiKeyLookupMock,
|
apiKeyLookupMock,
|
||||||
|
binaryClearCacheMock,
|
||||||
binaryResolveMock,
|
binaryResolveMock,
|
||||||
detectLocalAccountStateMock,
|
detectLocalAccountStateMock,
|
||||||
getCachedShellEnvMock,
|
getCachedShellEnvMock,
|
||||||
|
|
@ -24,12 +25,15 @@ const {
|
||||||
readAccountMock,
|
readAccountMock,
|
||||||
readAccountSnapshotMock,
|
readAccountSnapshotMock,
|
||||||
readRateLimitsMock,
|
readRateLimitsMock,
|
||||||
|
resolveInteractiveShellEnvBestEffortMock,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
binaryResolveMock: vi.fn(),
|
binaryResolveMock: vi.fn(),
|
||||||
|
binaryClearCacheMock: vi.fn(),
|
||||||
apiKeyHasPreferredMock: vi.fn(),
|
apiKeyHasPreferredMock: vi.fn(),
|
||||||
apiKeyLookupMock: vi.fn(),
|
apiKeyLookupMock: vi.fn(),
|
||||||
detectLocalAccountStateMock: vi.fn(),
|
detectLocalAccountStateMock: vi.fn(),
|
||||||
getCachedShellEnvMock: vi.fn(),
|
getCachedShellEnvMock: vi.fn(),
|
||||||
|
resolveInteractiveShellEnvBestEffortMock: vi.fn(),
|
||||||
readAccountMock: vi.fn(),
|
readAccountMock: vi.fn(),
|
||||||
readAccountSnapshotMock: vi.fn(),
|
readAccountSnapshotMock: vi.fn(),
|
||||||
readRateLimitsMock: vi.fn(),
|
readRateLimitsMock: vi.fn(),
|
||||||
|
|
@ -71,12 +75,14 @@ vi.mock('../../../../src/main/utils/shellEnv', async (importOriginal) => {
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
getCachedShellEnv: getCachedShellEnvMock,
|
getCachedShellEnv: getCachedShellEnvMock,
|
||||||
|
resolveInteractiveShellEnvBestEffort: resolveInteractiveShellEnvBestEffortMock,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/infrastructure/codexAppServer', () => ({
|
vi.mock('../../../../src/main/services/infrastructure/codexAppServer', () => ({
|
||||||
CodexBinaryResolver: {
|
CodexBinaryResolver: {
|
||||||
resolve: binaryResolveMock,
|
resolve: binaryResolveMock,
|
||||||
|
clearCache: binaryClearCacheMock,
|
||||||
},
|
},
|
||||||
CodexAppServerSessionFactory: class MockCodexAppServerSessionFactory {},
|
CodexAppServerSessionFactory: class MockCodexAppServerSessionFactory {},
|
||||||
JsonRpcStdioClient: class MockJsonRpcStdioClient {},
|
JsonRpcStdioClient: class MockJsonRpcStdioClient {},
|
||||||
|
|
@ -231,6 +237,9 @@ describe('createCodexAccountFeature', () => {
|
||||||
delete process.env.OPENAI_API_KEY;
|
delete process.env.OPENAI_API_KEY;
|
||||||
delete process.env.CODEX_API_KEY;
|
delete process.env.CODEX_API_KEY;
|
||||||
binaryResolveMock.mockResolvedValue('/usr/local/bin/codex');
|
binaryResolveMock.mockResolvedValue('/usr/local/bin/codex');
|
||||||
|
binaryClearCacheMock.mockReset();
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockReset();
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({});
|
||||||
apiKeyHasPreferredMock.mockResolvedValue(false);
|
apiKeyHasPreferredMock.mockResolvedValue(false);
|
||||||
apiKeyLookupMock.mockResolvedValue(null);
|
apiKeyLookupMock.mockResolvedValue(null);
|
||||||
detectLocalAccountStateMock.mockResolvedValue({
|
detectLocalAccountStateMock.mockResolvedValue({
|
||||||
|
|
@ -360,6 +369,69 @@ describe('createCodexAccountFeature', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('retries Codex binary discovery after cold shell env resolves before publishing runtime-missing', async () => {
|
||||||
|
binaryResolveMock.mockResolvedValueOnce(null).mockResolvedValue('/usr/local/bin/codex');
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({
|
||||||
|
PATH: '/usr/local/bin:/usr/bin:/bin',
|
||||||
|
});
|
||||||
|
readAccountMock.mockResolvedValue({
|
||||||
|
account: createAccountResponse(),
|
||||||
|
initialize: {
|
||||||
|
codexHome: '/Users/test/.codex',
|
||||||
|
platformFamily: 'unix',
|
||||||
|
platformOs: 'macos',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const feature = createCodexAccountFeature({
|
||||||
|
logger: createLoggerPort(),
|
||||||
|
configManager: createConfigManager('chatgpt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snapshot = await feature.refreshSnapshot();
|
||||||
|
|
||||||
|
expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
timeoutMs: 12_000,
|
||||||
|
fallbackEnv: process.env,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(binaryClearCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(binaryResolveMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(snapshot.appServerState).toBe('healthy');
|
||||||
|
expect(snapshot.launchReadinessState).toBe('ready_chatgpt');
|
||||||
|
expect(snapshot.launchIssueMessage).toBeNull();
|
||||||
|
} finally {
|
||||||
|
await feature.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still reports runtime-missing after the cold binary retry cannot find Codex', async () => {
|
||||||
|
binaryResolveMock.mockResolvedValue(null);
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({
|
||||||
|
PATH: '/usr/bin:/bin',
|
||||||
|
});
|
||||||
|
|
||||||
|
const feature = createCodexAccountFeature({
|
||||||
|
logger: createLoggerPort(),
|
||||||
|
configManager: createConfigManager('chatgpt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snapshot = await feature.refreshSnapshot();
|
||||||
|
|
||||||
|
expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(binaryClearCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(binaryResolveMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(snapshot.appServerState).toBe('runtime-missing');
|
||||||
|
expect(snapshot.launchReadinessState).toBe('runtime_missing');
|
||||||
|
expect(snapshot.launchIssueMessage).toContain('Codex CLI not found');
|
||||||
|
} finally {
|
||||||
|
await feature.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('reuses a fresh refresh snapshot when the request does not need stronger data', async () => {
|
it('reuses a fresh refresh snapshot when the request does not need stronger data', async () => {
|
||||||
readAccountMock.mockResolvedValue({
|
readAccountMock.mockResolvedValue({
|
||||||
account: createAccountResponse(),
|
account: createAccountResponse(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue