perf(startup): dedupe opencode version probes
This commit is contained in:
parent
b88ca42fe3
commit
a8ac52b6f3
2 changed files with 321 additions and 33 deletions
|
|
@ -27,6 +27,10 @@ const MAX_TARBALL_BYTES = 250 * 1024 * 1024;
|
||||||
const MAX_BINARY_BYTES = 350 * 1024 * 1024;
|
const MAX_BINARY_BYTES = 350 * 1024 * 1024;
|
||||||
const FETCH_TIMEOUT_MS = 60_000;
|
const FETCH_TIMEOUT_MS = 60_000;
|
||||||
const VERSION_TIMEOUT_MS = 10_000;
|
const VERSION_TIMEOUT_MS = 10_000;
|
||||||
|
const VERSION_PROBE_SUCCESS_CACHE_TTL_MS = 30_000;
|
||||||
|
const VERSION_PROBE_FAILURE_CACHE_TTL_MS = 5_000;
|
||||||
|
const RUNTIME_STATUS_SUCCESS_CACHE_TTL_MS = 30_000;
|
||||||
|
const RUNTIME_STATUS_FAILURE_CACHE_TTL_MS = 5_000;
|
||||||
|
|
||||||
interface NpmPackageMetadata {
|
interface NpmPackageMetadata {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -97,15 +101,8 @@ export async function resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(): Prom
|
||||||
if (!binaryPath) {
|
if (!binaryPath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
const version = await probeOpenCodeBinaryVersionCached(binaryPath);
|
||||||
await execCli(binaryPath, ['--version'], {
|
return version.ok ? binaryPath : null;
|
||||||
timeout: VERSION_TIMEOUT_MS,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
return binaryPath;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExecutableName(): string {
|
function getExecutableName(): string {
|
||||||
|
|
@ -173,6 +170,31 @@ type VerifiedOpenCodeBinaryProbe =
|
||||||
| { ok: true; binaryPath: string; version: string | null }
|
| { ok: true; binaryPath: string; version: string | null }
|
||||||
| { ok: false; firstFailure: { binaryPath: string; error: string } | null };
|
| { ok: false; firstFailure: { binaryPath: string; error: string } | null };
|
||||||
|
|
||||||
|
interface CachedVersionProbe {
|
||||||
|
result: OpenCodeBinaryVersionProbe;
|
||||||
|
cachedAt: number;
|
||||||
|
ttlMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedPathProbe {
|
||||||
|
result: VerifiedOpenCodeBinaryProbe;
|
||||||
|
cachedAt: number;
|
||||||
|
ttlMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedRuntimeBinaryResolve {
|
||||||
|
binaryPath: string | null;
|
||||||
|
cachedAt: number;
|
||||||
|
ttlMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionProbeCache = new Map<string, CachedVersionProbe>();
|
||||||
|
const versionProbeInFlight = new Map<string, Promise<OpenCodeBinaryVersionProbe>>();
|
||||||
|
const pathProbeCache = new Map<string, CachedPathProbe>();
|
||||||
|
const pathProbeInFlight = new Map<string, Promise<VerifiedOpenCodeBinaryProbe>>();
|
||||||
|
const runtimeBinaryResolveCache = new Map<string, CachedRuntimeBinaryResolve>();
|
||||||
|
const runtimeBinaryResolveInFlight = new Map<string, Promise<string | null>>();
|
||||||
|
|
||||||
async function probeOpenCodeBinaryVersion(binaryPath: string): Promise<OpenCodeBinaryVersionProbe> {
|
async function probeOpenCodeBinaryVersion(binaryPath: string): Promise<OpenCodeBinaryVersionProbe> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execCli(binaryPath, ['--version'], {
|
const { stdout } = await execCli(binaryPath, ['--version'], {
|
||||||
|
|
@ -190,6 +212,42 @@ function normalizeBinaryCandidateForCompare(binaryPath: string): string {
|
||||||
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVersionProbeTtlMs(result: OpenCodeBinaryVersionProbe): number {
|
||||||
|
return result.ok ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeOpenCodeBinaryVersionCached(
|
||||||
|
binaryPath: string
|
||||||
|
): Promise<OpenCodeBinaryVersionProbe> {
|
||||||
|
const cacheKey = normalizeBinaryCandidateForCompare(binaryPath);
|
||||||
|
const cached = versionProbeCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.cachedAt < cached.ttlMs) {
|
||||||
|
return cached.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inFlight = versionProbeInFlight.get(cacheKey);
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = probeOpenCodeBinaryVersion(binaryPath)
|
||||||
|
.then((result) => {
|
||||||
|
versionProbeCache.set(cacheKey, {
|
||||||
|
result,
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
ttlMs: getVersionProbeTtlMs(result),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (versionProbeInFlight.get(cacheKey) === request) {
|
||||||
|
versionProbeInFlight.delete(cacheKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
versionProbeInFlight.set(cacheKey, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
async function probeFirstWorkingOpenCodeBinaryCandidate(
|
async function probeFirstWorkingOpenCodeBinaryCandidate(
|
||||||
candidates: string[],
|
candidates: string[],
|
||||||
seen: Set<string>,
|
seen: Set<string>,
|
||||||
|
|
@ -202,7 +260,7 @@ async function probeFirstWorkingOpenCodeBinaryCandidate(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seen.add(normalized);
|
seen.add(normalized);
|
||||||
const version = await probeOpenCodeBinaryVersion(binaryPath);
|
const version = await probeOpenCodeBinaryVersionCached(binaryPath);
|
||||||
if (version.ok) {
|
if (version.ok) {
|
||||||
return { ok: true, binaryPath, version: version.version };
|
return { ok: true, binaryPath, version: version.version };
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +275,22 @@ interface OpenCodeRuntimeBinaryResolveOptions {
|
||||||
includeShellEnv?: boolean;
|
includeShellEnv?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPathProbeCacheKey(options: OpenCodeRuntimeBinaryResolveOptions = {}): string {
|
||||||
|
if (options.includeShellEnv === false) {
|
||||||
|
return 'no-shell-env';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `shell-env:${options.shellEnvTimeoutMs ?? RUNTIME_PATH_SHELL_ENV_TIMEOUT_MS}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPathProbeTtlMs(result: VerifiedOpenCodeBinaryProbe): number {
|
||||||
|
return result.ok ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuntimeBinaryResolveTtlMs(binaryPath: string | null): number {
|
||||||
|
return binaryPath ? VERSION_PROBE_SUCCESS_CACHE_TTL_MS : VERSION_PROBE_FAILURE_CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
async function probeFirstWorkingPathOpenCodeBinary(
|
async function probeFirstWorkingPathOpenCodeBinary(
|
||||||
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
||||||
): Promise<VerifiedOpenCodeBinaryProbe> {
|
): Promise<VerifiedOpenCodeBinaryProbe> {
|
||||||
|
|
@ -268,20 +342,86 @@ async function probeFirstWorkingPathOpenCodeBinary(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probeFirstWorkingPathOpenCodeBinaryCached(
|
||||||
|
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
||||||
|
): Promise<VerifiedOpenCodeBinaryProbe> {
|
||||||
|
const cacheKey = getPathProbeCacheKey(options);
|
||||||
|
const cached = pathProbeCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.cachedAt < cached.ttlMs) {
|
||||||
|
return cached.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inFlight = pathProbeInFlight.get(cacheKey);
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = probeFirstWorkingPathOpenCodeBinary(options)
|
||||||
|
.then((result) => {
|
||||||
|
pathProbeCache.set(cacheKey, {
|
||||||
|
result,
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
ttlMs: getPathProbeTtlMs(result),
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (pathProbeInFlight.get(cacheKey) === request) {
|
||||||
|
pathProbeInFlight.delete(cacheKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pathProbeInFlight.set(cacheKey, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveVerifiedPathOpenCodeBinaryPath(
|
async function resolveVerifiedPathOpenCodeBinaryPath(
|
||||||
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const result = await probeFirstWorkingPathOpenCodeBinary(options);
|
const result = await probeFirstWorkingPathOpenCodeBinaryCached(options);
|
||||||
return result.ok ? result.binaryPath : null;
|
return result.ok ? result.binaryPath : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearOpenCodeRuntimeBinaryResolverCache(): void {
|
||||||
|
versionProbeCache.clear();
|
||||||
|
versionProbeInFlight.clear();
|
||||||
|
pathProbeCache.clear();
|
||||||
|
pathProbeInFlight.clear();
|
||||||
|
runtimeBinaryResolveCache.clear();
|
||||||
|
runtimeBinaryResolveInFlight.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveVerifiedOpenCodeRuntimeBinaryPath(
|
export async function resolveVerifiedOpenCodeRuntimeBinaryPath(
|
||||||
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
options: OpenCodeRuntimeBinaryResolveOptions = {}
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
return (
|
const cacheKey = getPathProbeCacheKey(options);
|
||||||
|
const cached = runtimeBinaryResolveCache.get(cacheKey);
|
||||||
|
if (cached && Date.now() - cached.cachedAt < cached.ttlMs) {
|
||||||
|
return cached.binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inFlight = runtimeBinaryResolveInFlight.get(cacheKey);
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (async () =>
|
||||||
(await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()) ??
|
(await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()) ??
|
||||||
(await resolveVerifiedPathOpenCodeBinaryPath(options))
|
(await resolveVerifiedPathOpenCodeBinaryPath(options)))()
|
||||||
);
|
.then((binaryPath) => {
|
||||||
|
runtimeBinaryResolveCache.set(cacheKey, {
|
||||||
|
binaryPath,
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
ttlMs: getRuntimeBinaryResolveTtlMs(binaryPath),
|
||||||
|
});
|
||||||
|
return binaryPath;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (runtimeBinaryResolveInFlight.get(cacheKey) === request) {
|
||||||
|
runtimeBinaryResolveInFlight.delete(cacheKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
runtimeBinaryResolveInFlight.set(cacheKey, request);
|
||||||
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLinuxMuslRuntime(): boolean {
|
function isLinuxMuslRuntime(): boolean {
|
||||||
|
|
@ -511,6 +651,8 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
private installPromise: Promise<OpenCodeRuntimeStatus> | null = null;
|
private installPromise: Promise<OpenCodeRuntimeStatus> | null = null;
|
||||||
private latestStatus: OpenCodeRuntimeStatus | null = null;
|
private latestStatus: OpenCodeRuntimeStatus | null = null;
|
||||||
|
private latestStatusAt = 0;
|
||||||
|
private statusPromise: Promise<OpenCodeRuntimeStatus> | null = null;
|
||||||
|
|
||||||
setMainWindow(win: BrowserWindow | null): void {
|
setMainWindow(win: BrowserWindow | null): void {
|
||||||
this.mainWindow = win;
|
this.mainWindow = win;
|
||||||
|
|
@ -518,16 +660,40 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
|
|
||||||
invalidateStatusCache(): void {
|
invalidateStatusCache(): void {
|
||||||
this.latestStatus = null;
|
this.latestStatus = null;
|
||||||
|
this.latestStatusAt = 0;
|
||||||
|
this.statusPromise = null;
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatus(): Promise<OpenCodeRuntimeStatus> {
|
async getStatus(): Promise<OpenCodeRuntimeStatus> {
|
||||||
if (this.installPromise && this.latestStatus) {
|
if (this.installPromise && this.latestStatus) {
|
||||||
return this.latestStatus;
|
return this.latestStatus;
|
||||||
}
|
}
|
||||||
|
if (this.installPromise) {
|
||||||
|
return this.installPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.latestStatus && Date.now() - this.latestStatusAt < this.getStatusCacheTtlMs()) {
|
||||||
|
return this.latestStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusPromise) {
|
||||||
|
return this.statusPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = this.resolveStatus().finally(() => {
|
||||||
|
if (this.statusPromise === request) {
|
||||||
|
this.statusPromise = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.statusPromise = request;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveStatus(): Promise<OpenCodeRuntimeStatus> {
|
||||||
const appManagedStatus = await this.getAppManagedStatus();
|
const appManagedStatus = await this.getAppManagedStatus();
|
||||||
if (appManagedStatus.installed) {
|
if (appManagedStatus.installed) {
|
||||||
this.latestStatus = appManagedStatus;
|
this.rememberStatus(appManagedStatus);
|
||||||
return appManagedStatus;
|
return appManagedStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -538,7 +704,7 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
appManagedStatus.state !== 'failed'
|
appManagedStatus.state !== 'failed'
|
||||||
? pathStatus
|
? pathStatus
|
||||||
: appManagedStatus;
|
: appManagedStatus;
|
||||||
this.latestStatus = status;
|
this.rememberStatus(status);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,10 +719,21 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private publish(status: OpenCodeRuntimeStatus): void {
|
private publish(status: OpenCodeRuntimeStatus): void {
|
||||||
this.latestStatus = status;
|
this.rememberStatus(status);
|
||||||
safeSendToRenderer(this.mainWindow, CHANNEL, status);
|
safeSendToRenderer(this.mainWindow, CHANNEL, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private rememberStatus(status: OpenCodeRuntimeStatus): void {
|
||||||
|
this.latestStatus = status;
|
||||||
|
this.latestStatusAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusCacheTtlMs(): number {
|
||||||
|
return this.latestStatus?.installed === true
|
||||||
|
? RUNTIME_STATUS_SUCCESS_CACHE_TTL_MS
|
||||||
|
: RUNTIME_STATUS_FAILURE_CACHE_TTL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
private publishProgress(progress: OpenCodeRuntimeInstallProgress): void {
|
private publishProgress(progress: OpenCodeRuntimeInstallProgress): void {
|
||||||
this.publish({
|
this.publish({
|
||||||
installed: false,
|
installed: false,
|
||||||
|
|
@ -571,32 +748,28 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
if (!isAbsoluteExistingFile(manifest?.binaryPath)) {
|
if (!isAbsoluteExistingFile(manifest?.binaryPath)) {
|
||||||
return { installed: false, source: 'missing', state: 'idle' };
|
return { installed: false, source: 'missing', state: 'idle' };
|
||||||
}
|
}
|
||||||
try {
|
const version = await probeOpenCodeBinaryVersionCached(manifest.binaryPath);
|
||||||
const { stdout } = await execCli(manifest.binaryPath, ['--version'], {
|
if (version.ok) {
|
||||||
timeout: VERSION_TIMEOUT_MS,
|
|
||||||
windowsHide: true,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
installed: true,
|
installed: true,
|
||||||
binaryPath: manifest.binaryPath,
|
binaryPath: manifest.binaryPath,
|
||||||
version: stdout.trim() || manifest.version,
|
version: version.version ?? manifest.version,
|
||||||
source: 'app-managed',
|
source: 'app-managed',
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
binaryPath: manifest.binaryPath,
|
|
||||||
version: manifest.version,
|
|
||||||
source: 'app-managed',
|
|
||||||
state: 'failed',
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
installed: false,
|
||||||
|
binaryPath: manifest.binaryPath,
|
||||||
|
version: manifest.version,
|
||||||
|
source: 'app-managed',
|
||||||
|
state: 'failed',
|
||||||
|
error: version.error,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPathStatus(): Promise<OpenCodeRuntimeStatus> {
|
private async getPathStatus(): Promise<OpenCodeRuntimeStatus> {
|
||||||
const result = await probeFirstWorkingPathOpenCodeBinary();
|
const result = await probeFirstWorkingPathOpenCodeBinaryCached();
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
return {
|
return {
|
||||||
installed: true,
|
installed: true,
|
||||||
|
|
@ -687,6 +860,7 @@ export class OpenCodeRuntimeInstallerService {
|
||||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache();
|
||||||
|
|
||||||
const status: OpenCodeRuntimeStatus = {
|
const status: OpenCodeRuntimeStatus = {
|
||||||
installed: true,
|
installed: true,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ vi.mock('@main/utils/shellEnv', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache,
|
||||||
extractOpenCodeRuntimeBinaryFromTarball,
|
extractOpenCodeRuntimeBinaryFromTarball,
|
||||||
getOpenCodeRuntimePlatformCandidates,
|
getOpenCodeRuntimePlatformCandidates,
|
||||||
OpenCodeRuntimeInstallerService,
|
OpenCodeRuntimeInstallerService,
|
||||||
|
|
@ -41,6 +42,20 @@ import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
||||||
let tempRoot: string | null = null;
|
let tempRoot: string | null = null;
|
||||||
let originalPath: string | undefined;
|
let originalPath: string | undefined;
|
||||||
|
|
||||||
|
function deferred<T>(): {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T) => void;
|
||||||
|
reject: (error: unknown) => void;
|
||||||
|
} {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
let reject!: (error: unknown) => void;
|
||||||
|
const promise = new Promise<T>((promiseResolve, promiseReject) => {
|
||||||
|
resolve = promiseResolve;
|
||||||
|
reject = promiseReject;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
}
|
||||||
|
|
||||||
function writeOctal(header: Buffer, offset: number, length: number, value: number): void {
|
function writeOctal(header: Buffer, offset: number, length: number, value: number): void {
|
||||||
const encoded = value
|
const encoded = value
|
||||||
.toString(8)
|
.toString(8)
|
||||||
|
|
@ -84,6 +99,7 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
setAppDataBasePath(tempRoot);
|
setAppDataBasePath(tempRoot);
|
||||||
originalPath = process.env.PATH;
|
originalPath = process.env.PATH;
|
||||||
process.env.PATH = '';
|
process.env.PATH = '';
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache();
|
||||||
execCliMock.mockReset();
|
execCliMock.mockReset();
|
||||||
execCliMock.mockResolvedValue({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
execCliMock.mockResolvedValue({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||||
buildMergedCliPathMock.mockReset();
|
buildMergedCliPathMock.mockReset();
|
||||||
|
|
@ -97,6 +113,7 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache();
|
||||||
setAppDataBasePath(null);
|
setAppDataBasePath(null);
|
||||||
if (originalPath === undefined) {
|
if (originalPath === undefined) {
|
||||||
delete process.env.PATH;
|
delete process.env.PATH;
|
||||||
|
|
@ -194,11 +211,53 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearOpenCodeRuntimeBinaryResolverCache();
|
||||||
execCliMock.mockRejectedValueOnce(new Error('broken binary'));
|
execCliMock.mockRejectedValueOnce(new Error('broken binary'));
|
||||||
|
|
||||||
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBeNull();
|
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces concurrent app-managed OpenCode verification probes', async () => {
|
||||||
|
const binaryPath = path.join(
|
||||||
|
tempRoot!,
|
||||||
|
'data',
|
||||||
|
'runtimes',
|
||||||
|
'opencode',
|
||||||
|
'versions',
|
||||||
|
'1.0.0',
|
||||||
|
'opencode-test',
|
||||||
|
'opencode'
|
||||||
|
);
|
||||||
|
const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'opencode', 'current.json');
|
||||||
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
||||||
|
await writeFile(binaryPath, 'binary', { mode: 0o755 });
|
||||||
|
await writeFile(
|
||||||
|
manifestPath,
|
||||||
|
`${JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
version: '1.0.0',
|
||||||
|
platformPackage: 'opencode-test',
|
||||||
|
binaryPath,
|
||||||
|
integrity: 'sha512-test',
|
||||||
|
installedAt: '2026-05-12T00:00:00.000Z',
|
||||||
|
})}\n`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
const versionProbe = deferred<{ stdout: string; stderr: string }>();
|
||||||
|
execCliMock.mockReturnValue(versionProbe.promise);
|
||||||
|
|
||||||
|
const first = resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath();
|
||||||
|
const second = resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath();
|
||||||
|
await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||||
|
await expect(Promise.all([first, second])).resolves.toEqual([binaryPath, binaryPath]);
|
||||||
|
|
||||||
|
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBe(binaryPath);
|
||||||
|
expect(execCliMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns a verified OpenCode binary from best-effort shell PATH when app-managed runtime is absent', async () => {
|
it('returns a verified OpenCode binary from best-effort shell PATH when app-managed runtime is absent', async () => {
|
||||||
const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode');
|
const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode');
|
||||||
await mkdir(path.dirname(binaryPath), { recursive: true });
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
|
@ -222,6 +281,61 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces concurrent verified OpenCode PATH probes and reuses the warm result', async () => {
|
||||||
|
const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode');
|
||||||
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
await writeFile(binaryPath, 'binary', { mode: 0o755 });
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({
|
||||||
|
PATH: path.dirname(binaryPath),
|
||||||
|
});
|
||||||
|
getShellPreferredHomeMock.mockReturnValue(tempRoot!);
|
||||||
|
const versionProbe = deferred<{ stdout: string; stderr: string }>();
|
||||||
|
execCliMock.mockReturnValue(versionProbe.promise);
|
||||||
|
|
||||||
|
const first = resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 });
|
||||||
|
const second = resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 });
|
||||||
|
await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||||
|
await expect(Promise.all([first, second])).resolves.toEqual([binaryPath, binaryPath]);
|
||||||
|
|
||||||
|
await expect(resolveVerifiedOpenCodeRuntimeBinaryPath({ shellEnvTimeoutMs: 0 })).resolves.toBe(
|
||||||
|
binaryPath
|
||||||
|
);
|
||||||
|
expect(execCliMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(resolveInteractiveShellEnvBestEffortMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coalesces concurrent OpenCode runtime status checks and serves a short warm cache', async () => {
|
||||||
|
const binaryPath = path.join(tempRoot!, 'homebrew', 'bin', 'opencode');
|
||||||
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
await writeFile(binaryPath, 'binary', { mode: 0o755 });
|
||||||
|
resolveInteractiveShellEnvBestEffortMock.mockResolvedValue({
|
||||||
|
PATH: path.dirname(binaryPath),
|
||||||
|
});
|
||||||
|
getShellPreferredHomeMock.mockReturnValue(tempRoot!);
|
||||||
|
const versionProbe = deferred<{ stdout: string; stderr: string }>();
|
||||||
|
execCliMock.mockReturnValue(versionProbe.promise);
|
||||||
|
const service = new OpenCodeRuntimeInstallerService();
|
||||||
|
|
||||||
|
const first = service.getStatus();
|
||||||
|
const second = service.getStatus();
|
||||||
|
await vi.waitFor(() => expect(execCliMock).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
versionProbe.resolve({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||||
|
await expect(Promise.all([first, second])).resolves.toMatchObject([
|
||||||
|
{ installed: true, source: 'path', binaryPath },
|
||||||
|
{ installed: true, source: 'path', binaryPath },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(service.getStatus()).resolves.toMatchObject({
|
||||||
|
installed: true,
|
||||||
|
source: 'path',
|
||||||
|
binaryPath,
|
||||||
|
});
|
||||||
|
expect(execCliMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns a verified OpenCode binary from the merged CLI PATH after zero-wait shell fallback', async () => {
|
it('returns a verified OpenCode binary from the merged CLI PATH after zero-wait shell fallback', async () => {
|
||||||
const binaryPath = path.join(tempRoot!, 'merged-cli-path', 'bin', 'opencode');
|
const binaryPath = path.join(tempRoot!, 'merged-cli-path', 'bin', 'opencode');
|
||||||
await mkdir(path.dirname(binaryPath), { recursive: true });
|
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue