fix(opencode): preserve command preflight context
This commit is contained in:
parent
d5894c029d
commit
933e580d03
9 changed files with 342 additions and 32 deletions
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"version": "0.0.42",
|
||||
"sourceRef": "v0.0.42",
|
||||
"version": "0.0.43",
|
||||
"sourceRef": "v0.0.43",
|
||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||
"releaseRepository": "777genius/agent-teams-ai",
|
||||
"releaseTag": "v2.0.0",
|
||||
"assets": {
|
||||
"darwin-arm64": {
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.42.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.43.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.42.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.43.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"linux-x64": {
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.42.tar.gz",
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.43.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"win32-x64": {
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.42.zip",
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.43.zip",
|
||||
"archiveKind": "zip",
|
||||
"binaryName": "claude-multimodel.exe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import path from 'node:path';
|
|||
const CHILD_CLOSE_GRACE_MS = 3_000;
|
||||
const CHILD_FORCE_CLOSE_GRACE_MS = 1_000;
|
||||
const TASKKILL_TIMEOUT_MS = 5_000;
|
||||
const OPENCODE_HEALTH_FETCH_TIMEOUT_MS = 1_000;
|
||||
|
||||
export async function preflightOpenCodeLiveEnvironment(input) {
|
||||
const repoRoot = input.repoRoot;
|
||||
|
|
@ -125,13 +126,12 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
|||
return { ok: false, reason: output || `process exited with code ${child.exitCode}` };
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/global/health`);
|
||||
if (response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (data?.healthy === true) {
|
||||
return { ok: true };
|
||||
}
|
||||
const response = await fetchOpenCodeHealth(port);
|
||||
if (isHealthyOpenCodeHostResponse(response)) {
|
||||
response.body?.cancel().catch(() => undefined);
|
||||
return { ok: true };
|
||||
}
|
||||
response.body?.cancel().catch(() => undefined);
|
||||
} catch {
|
||||
// Host is still starting.
|
||||
}
|
||||
|
|
@ -143,6 +143,22 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchOpenCodeHealth(port) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OPENCODE_HEALTH_FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(`http://127.0.0.1:${port}/global/health`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
function isHealthyOpenCodeHostResponse(response) {
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
async function stopChild(child, options = {}) {
|
||||
const platform = options.platform ?? process.platform;
|
||||
const killProcessTree = options.killProcessTree ?? taskkillProcessTree;
|
||||
|
|
@ -271,6 +287,7 @@ function compactOutput(value) {
|
|||
}
|
||||
|
||||
export const __opencodeLivePreflightTestHooks = {
|
||||
isHealthyOpenCodeHostResponse,
|
||||
stopChild,
|
||||
taskkillProcessTree,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ const env = {
|
|||
PROVIDER_LAUNCH_STRESS_ANTHROPIC_AUTH:
|
||||
process.env.PROVIDER_LAUNCH_STRESS_ANTHROPIC_AUTH?.trim() ||
|
||||
(process.env.ANTHROPIC_API_KEY?.trim() ? 'api-key' : 'subscription'),
|
||||
CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS:
|
||||
process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS?.trim() || '90000',
|
||||
CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS:
|
||||
process.env.CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS?.trim() || '30000',
|
||||
OPENCODE_E2E: '1',
|
||||
OPENCODE_E2E_USE_REAL_APP_CREDENTIALS: '1',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export class OpenCodeBridgeCommandClient {
|
|||
body,
|
||||
};
|
||||
const inputPath = await this.writeInputFile(envelope);
|
||||
const outputPath = `${inputPath}.output.json`;
|
||||
|
||||
try {
|
||||
const maxAttempts =
|
||||
|
|
@ -169,13 +170,22 @@ export class OpenCodeBridgeCommandClient {
|
|||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const processResult = await this.processRunner.run({
|
||||
binaryPath: this.binaryPath,
|
||||
args: ['runtime', 'opencode-command', '--json', '--input', inputPath],
|
||||
args: [
|
||||
'runtime',
|
||||
'opencode-command',
|
||||
'--json',
|
||||
'--input',
|
||||
inputPath,
|
||||
'--output',
|
||||
outputPath,
|
||||
],
|
||||
cwd: resolveOpenCodeBridgeProcessCwd(this.binaryPath, options.cwd),
|
||||
timeoutMs: options.timeoutMs,
|
||||
stdoutLimitBytes: options.stdoutLimitBytes ?? DEFAULT_STDOUT_LIMIT_BYTES,
|
||||
stderrLimitBytes: options.stderrLimitBytes ?? DEFAULT_STDERR_LIMIT_BYTES,
|
||||
env: await this.resolveEnv(),
|
||||
});
|
||||
const stdout = await this.readBridgeOutput(processResult.stdout, outputPath);
|
||||
|
||||
if (processResult.timedOut) {
|
||||
return this.contractFailure(
|
||||
|
|
@ -204,7 +214,7 @@ export class OpenCodeBridgeCommandClient {
|
|||
);
|
||||
}
|
||||
|
||||
const parsed = parseSingleBridgeJsonResult<TData>(processResult.stdout);
|
||||
const parsed = parseSingleBridgeJsonResult<TData>(stdout);
|
||||
if (!parsed.ok) {
|
||||
if (shouldRetryEmptyReadinessStdout(command, parsed.error, attempt, maxAttempts)) {
|
||||
await sleep(EMPTY_STDOUT_READINESS_RETRY_DELAY_MS);
|
||||
|
|
@ -212,7 +222,7 @@ export class OpenCodeBridgeCommandClient {
|
|||
}
|
||||
|
||||
return this.contractFailure(envelope, 'contract_violation', parsed.error, false, {
|
||||
stdoutPreview: redactBridgeDiagnosticText(processResult.stdout.slice(0, 2_000)),
|
||||
stdoutPreview: redactBridgeDiagnosticText(stdout.slice(0, 2_000)),
|
||||
stderrPreview: redactBridgeDiagnosticText(processResult.stderr.slice(0, 2_000)),
|
||||
attempts: attempt,
|
||||
});
|
||||
|
|
@ -239,6 +249,18 @@ export class OpenCodeBridgeCommandClient {
|
|||
if (!this.keepInputFile) {
|
||||
await fs.unlink(inputPath).catch(() => undefined);
|
||||
}
|
||||
await fs.unlink(outputPath).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private async readBridgeOutput(stdout: string, outputPath: string): Promise<string> {
|
||||
if (stdout.trim().length > 0) {
|
||||
return stdout;
|
||||
}
|
||||
try {
|
||||
return await fs.readFile(outputPath, 'utf8');
|
||||
} catch {
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@ type SelectedModelChecksByProvider = ReadonlyMap<
|
|||
readonly ProviderModelCheckSignatureInput[]
|
||||
>;
|
||||
|
||||
function getCodexPrepareRuntimeSignature(
|
||||
codex: NonNullable<NonNullable<CliProviderStatus['connection']>['codex']>
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
preferredAuthMode: codex.preferredAuthMode,
|
||||
effectiveAuthMode: codex.effectiveAuthMode,
|
||||
managedAccountType: codex.managedAccount?.type ?? null,
|
||||
requiresOpenaiAuth: codex.requiresOpenaiAuth ?? null,
|
||||
launchAllowed: codex.launchAllowed,
|
||||
launchReadinessState: codex.launchAllowed ? 'launchable' : codex.launchReadinessState,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeModelIds(modelIds: readonly string[] | null | undefined): string[] {
|
||||
return Array.from(
|
||||
new Set((modelIds ?? []).map((modelId) => modelId.trim()).filter(Boolean))
|
||||
|
|
@ -100,22 +113,7 @@ export function buildProviderPrepareRuntimeStatusSignature(
|
|||
apiKeyConfigured: provider.connection.apiKeyConfigured,
|
||||
apiKeySource: provider.connection.apiKeySource ?? null,
|
||||
codex: provider.connection.codex
|
||||
? {
|
||||
preferredAuthMode: provider.connection.codex.preferredAuthMode,
|
||||
effectiveAuthMode: provider.connection.codex.effectiveAuthMode,
|
||||
appServerState: provider.connection.codex.appServerState,
|
||||
managedAccountType: provider.connection.codex.managedAccount?.type ?? null,
|
||||
managedAccountEmail: provider.connection.codex.managedAccount?.email ?? null,
|
||||
requiresOpenaiAuth: provider.connection.codex.requiresOpenaiAuth ?? null,
|
||||
localAccountArtifactsPresent:
|
||||
provider.connection.codex.localAccountArtifactsPresent ?? null,
|
||||
localActiveChatgptAccountPresent:
|
||||
provider.connection.codex.localActiveChatgptAccountPresent ?? null,
|
||||
loginStatus: provider.connection.codex.login?.status ?? null,
|
||||
launchAllowed: provider.connection.codex.launchAllowed,
|
||||
launchIssueMessage: provider.connection.codex.launchIssueMessage ?? null,
|
||||
launchReadinessState: provider.connection.codex.launchReadinessState,
|
||||
}
|
||||
? getCodexPrepareRuntimeSignature(provider.connection.codex)
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,15 @@ describe('OpenCodeBridgeCommandClient', () => {
|
|||
expect(runner.calls).toHaveLength(1);
|
||||
expect(runner.calls[0]).toMatchObject({
|
||||
binaryPath: '/usr/local/bin/agent-teams-controller',
|
||||
args: ['runtime', 'opencode-command', '--json', '--input', expect.any(String)],
|
||||
args: [
|
||||
'runtime',
|
||||
'opencode-command',
|
||||
'--json',
|
||||
'--input',
|
||||
expect.any(String),
|
||||
'--output',
|
||||
expect.any(String),
|
||||
],
|
||||
cwd: '/tmp/project',
|
||||
timeoutMs: 10_000,
|
||||
env: expect.objectContaining({
|
||||
|
|
@ -68,6 +76,7 @@ describe('OpenCodeBridgeCommandClient', () => {
|
|||
});
|
||||
|
||||
const inputPath = runner.calls[0].args[4];
|
||||
const outputPath = runner.calls[0].args[6];
|
||||
expect(JSON.parse(await runner.readInputEnvelope(0))).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
requestId: 'req-1',
|
||||
|
|
@ -77,6 +86,33 @@ describe('OpenCodeBridgeCommandClient', () => {
|
|||
body: { runId: 'run-1' },
|
||||
});
|
||||
await expect(fs.access(inputPath)).rejects.toThrow();
|
||||
await expect(fs.access(outputPath)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('reads bridge JSON from the output file when stdout is empty', async () => {
|
||||
runner.nextResult = {
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exitCode: 0,
|
||||
timedOut: false,
|
||||
};
|
||||
runner.nextOutputFileContents = `${JSON.stringify(bridgeSuccess({ data: { runId: 'run-1' } }))}\n`;
|
||||
const client = createClient();
|
||||
|
||||
const result = await client.execute(
|
||||
'opencode.launchTeam',
|
||||
{ runId: 'run-1' },
|
||||
{
|
||||
cwd: '/tmp/project',
|
||||
timeoutMs: 10_000,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
requestId: 'req-1',
|
||||
command: 'opencode.launchTeam',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed when stdout contains logs plus json', async () => {
|
||||
|
|
@ -455,10 +491,17 @@ class FakeBridgeProcessRunner implements OpenCodeBridgeProcessRunner {
|
|||
exitCode: 0,
|
||||
timedOut: false,
|
||||
};
|
||||
nextOutputFileContents: string | null = null;
|
||||
|
||||
async run(input: OpenCodeBridgeProcessRunInput): Promise<OpenCodeBridgeProcessRunResult> {
|
||||
this.calls.push(input);
|
||||
this.inputEnvelopes.push(await fs.readFile(input.args[4], 'utf8'));
|
||||
const outputFlagIndex = input.args.indexOf('--output');
|
||||
const outputPath = outputFlagIndex >= 0 ? input.args[outputFlagIndex + 1] : undefined;
|
||||
if (this.nextOutputFileContents !== null && outputPath) {
|
||||
await fs.writeFile(outputPath, this.nextOutputFileContents, 'utf8');
|
||||
this.nextOutputFileContents = null;
|
||||
}
|
||||
return this.nextResults.shift() ?? this.nextResult;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ liveDescribe('provider launch stress live e2e', () => {
|
|||
let previousNodeEnv: string | undefined;
|
||||
let previousAnthropicApiKey: string | undefined;
|
||||
let previousAnthropicAuthToken: string | undefined;
|
||||
let previousRuntimeReadyTimeout: string | undefined;
|
||||
let previousInboxPollerReadyTimeout: string | undefined;
|
||||
let previousClaudeJsonConfig: string | null | undefined;
|
||||
const activeScenarios: ActiveScenario[] = [];
|
||||
|
||||
|
|
@ -136,10 +138,16 @@ liveDescribe('provider launch stress live e2e', () => {
|
|||
previousNodeEnv = process.env.NODE_ENV;
|
||||
previousAnthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
previousAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN;
|
||||
previousRuntimeReadyTimeout = process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS;
|
||||
previousInboxPollerReadyTimeout = process.env.CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS;
|
||||
|
||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH =
|
||||
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
|
||||
process.env.CLAUDE_TEAM_CLI_FLAVOR = 'agent_teams_orchestrator';
|
||||
process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS =
|
||||
process.env.CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS?.trim() || '90000';
|
||||
process.env.CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS =
|
||||
process.env.CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS?.trim() || '30000';
|
||||
process.env.CODEX_HOME = resolveConnectedCodexHome(previousCodexHome);
|
||||
process.env.HOME = usingAnthropicSubscriptionAuth() ? os.userInfo().homedir : tempHome;
|
||||
process.env.USERPROFILE = usingAnthropicSubscriptionAuth() ? os.userInfo().homedir : tempHome;
|
||||
|
|
@ -170,6 +178,8 @@ liveDescribe('provider launch stress live e2e', () => {
|
|||
restoreEnv('NODE_ENV', previousNodeEnv);
|
||||
restoreEnv('ANTHROPIC_API_KEY', previousAnthropicApiKey);
|
||||
restoreEnv('ANTHROPIC_AUTH_TOKEN', previousAnthropicAuthToken);
|
||||
restoreEnv('CLAUDE_TEAM_PROCESS_RUNTIME_READY_TIMEOUT_MS', previousRuntimeReadyTimeout);
|
||||
restoreEnv('CLAUDE_TEAM_PROCESS_INBOX_POLLER_READY_TIMEOUT_MS', previousInboxPollerReadyTimeout);
|
||||
|
||||
if (process.env.PROVIDER_LAUNCH_STRESS_KEEP_TEMP === '1') {
|
||||
process.stderr.write(`[ProviderLaunchStress.live] preserved temp dir: ${tempDir}\n`);
|
||||
|
|
|
|||
|
|
@ -600,6 +600,213 @@ describe('providerPrepareRequestSignature', () => {
|
|||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it('ignores launchable Codex account telemetry churn', () => {
|
||||
const providerIds = ['codex'] as const;
|
||||
const first = buildProviderPrepareRuntimeStatusSignature(
|
||||
providerIds,
|
||||
providerStatusMap([
|
||||
[
|
||||
'codex',
|
||||
{
|
||||
providerId: 'codex',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'chatgpt',
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: true,
|
||||
configurableAuthModes: ['auto', 'chatgpt', 'api_key'],
|
||||
configuredAuthMode: 'chatgpt',
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
codex: {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: 'chatgpt',
|
||||
appServerState: 'healthy',
|
||||
appServerStatusMessage: null,
|
||||
managedAccount: {
|
||||
type: 'chatgpt',
|
||||
email: 'user@example.com',
|
||||
planType: 'plus',
|
||||
},
|
||||
requiresOpenaiAuth: false,
|
||||
localAccountArtifactsPresent: true,
|
||||
localActiveChatgptAccountPresent: true,
|
||||
login: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
},
|
||||
rateLimits: null,
|
||||
launchAllowed: true,
|
||||
launchIssueMessage: null,
|
||||
launchReadinessState: 'ready_chatgpt',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
const second = buildProviderPrepareRuntimeStatusSignature(
|
||||
providerIds,
|
||||
providerStatusMap([
|
||||
[
|
||||
'codex',
|
||||
{
|
||||
providerId: 'codex',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'chatgpt',
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: true,
|
||||
configurableAuthModes: ['auto', 'chatgpt', 'api_key'],
|
||||
configuredAuthMode: 'chatgpt',
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
codex: {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: 'chatgpt',
|
||||
appServerState: 'degraded',
|
||||
appServerStatusMessage: 'rate limits refresh failed',
|
||||
managedAccount: {
|
||||
type: 'chatgpt',
|
||||
email: 'user@example.com',
|
||||
planType: 'plus',
|
||||
},
|
||||
requiresOpenaiAuth: false,
|
||||
localAccountArtifactsPresent: false,
|
||||
localActiveChatgptAccountPresent: true,
|
||||
login: {
|
||||
status: 'pending',
|
||||
error: null,
|
||||
startedAt: '2026-05-19T00:00:00.000Z',
|
||||
},
|
||||
rateLimits: {
|
||||
limitId: 'codex',
|
||||
limitName: null,
|
||||
primary: {
|
||||
usedPercent: 87,
|
||||
windowDurationMins: 300,
|
||||
resetsAt: 1_779_120_000_000,
|
||||
},
|
||||
secondary: null,
|
||||
credits: null,
|
||||
planType: 'plus',
|
||||
},
|
||||
launchAllowed: true,
|
||||
launchIssueMessage: 'Ready with degraded account verification.',
|
||||
launchReadinessState: 'warning_degraded_but_launchable',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
expect(first).toBe(second);
|
||||
});
|
||||
|
||||
it('changes the Codex runtime signature when launchability changes', () => {
|
||||
const providerIds = ['codex'] as const;
|
||||
const ready = buildProviderPrepareRuntimeStatusSignature(
|
||||
providerIds,
|
||||
providerStatusMap([
|
||||
[
|
||||
'codex',
|
||||
{
|
||||
providerId: 'codex',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'chatgpt',
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: true,
|
||||
configurableAuthModes: ['auto', 'chatgpt', 'api_key'],
|
||||
configuredAuthMode: 'chatgpt',
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
codex: {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: 'chatgpt',
|
||||
appServerState: 'healthy',
|
||||
appServerStatusMessage: null,
|
||||
managedAccount: {
|
||||
type: 'chatgpt',
|
||||
email: 'user@example.com',
|
||||
planType: 'plus',
|
||||
},
|
||||
requiresOpenaiAuth: false,
|
||||
localAccountArtifactsPresent: true,
|
||||
localActiveChatgptAccountPresent: true,
|
||||
login: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
},
|
||||
rateLimits: null,
|
||||
launchAllowed: true,
|
||||
launchIssueMessage: null,
|
||||
launchReadinessState: 'ready_chatgpt',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
const missingAuth = buildProviderPrepareRuntimeStatusSignature(
|
||||
providerIds,
|
||||
providerStatusMap([
|
||||
[
|
||||
'codex',
|
||||
{
|
||||
providerId: 'codex',
|
||||
supported: true,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: true,
|
||||
configurableAuthModes: ['auto', 'chatgpt', 'api_key'],
|
||||
configuredAuthMode: 'chatgpt',
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
codex: {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: null,
|
||||
appServerState: 'healthy',
|
||||
appServerStatusMessage: null,
|
||||
managedAccount: null,
|
||||
requiresOpenaiAuth: true,
|
||||
localAccountArtifactsPresent: true,
|
||||
localActiveChatgptAccountPresent: false,
|
||||
login: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
},
|
||||
rateLimits: null,
|
||||
launchAllowed: false,
|
||||
launchIssueMessage: 'Connect a ChatGPT account.',
|
||||
launchReadinessState: 'missing_auth',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
expect(ready).not.toBe(missingAuth);
|
||||
});
|
||||
|
||||
it('ignores volatile member draft ids in provider prepare signatures', () => {
|
||||
const first = buildProviderPrepareMembersSignature([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface StopChildOptions {
|
|||
|
||||
interface OpenCodeLivePreflightTestHooks {
|
||||
__opencodeLivePreflightTestHooks: {
|
||||
isHealthyOpenCodeHostResponse(response: { ok: boolean }): boolean;
|
||||
stopChild(child: FakeChild, options?: StopChildOptions): Promise<void>;
|
||||
taskkillProcessTree(pid: number): Promise<void>;
|
||||
};
|
||||
|
|
@ -38,6 +39,14 @@ describe('opencode live preflight cleanup', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('accepts an HTTP 2xx OpenCode health response without requiring a JSON body', async () => {
|
||||
const { isHealthyOpenCodeHostResponse } = (await loadTestHooks())
|
||||
.__opencodeLivePreflightTestHooks;
|
||||
|
||||
expect(isHealthyOpenCodeHostResponse({ ok: true })).toBe(true);
|
||||
expect(isHealthyOpenCodeHostResponse({ ok: false })).toBe(false);
|
||||
});
|
||||
|
||||
it('waits for child close after Windows process-tree cleanup', async () => {
|
||||
const { stopChild } = (await loadTestHooks()).__opencodeLivePreflightTestHooks;
|
||||
const child = new FakeChild({ pid: 1234 });
|
||||
|
|
|
|||
Loading…
Reference in a new issue