fix: prevent bundled OpenCode self-upgrades (#2525)
* fix: prevent bundled OpenCode self-upgrades * feat(vscode): support OpenCode upgrades * fix: refresh OpenCode update status on runtime switch --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
c0405d3fa4
commit
c88dd16d2a
@@ -69,6 +69,7 @@ import { createServerUtilsRuntime } from './lib/opencode/server-utils-runtime.js
|
||||
import { createStaticRoutesRuntime } from './lib/opencode/static-routes-runtime.js';
|
||||
import { createSettingsRuntime } from './lib/opencode/settings-runtime.js';
|
||||
import { createOpenCodeResolutionRuntime } from './lib/opencode/opencode-resolution-runtime.js';
|
||||
import { resolveOpenCodeUpgradeCapability } from './lib/opencode/upgrade-capability.js';
|
||||
import { createBootstrapRuntime } from './lib/opencode/bootstrap-runtime.js';
|
||||
import { createSessionRuntime } from './lib/opencode/session-runtime.js';
|
||||
import { createOpenCodeWatcherRuntime } from './lib/opencode/watcher.js';
|
||||
@@ -666,6 +667,7 @@ const getLoginShellEnvSnapshot = (...args) => openCodeEnvRuntime.getLoginShellEn
|
||||
const ensureOpencodeCliEnv = (...args) => openCodeEnvRuntime.ensureOpencodeCliEnv(...args);
|
||||
const applyOpencodeBinaryFromSettings = (...args) => openCodeEnvRuntime.applyOpencodeBinaryFromSettings(...args);
|
||||
const resolveOpencodeCliPath = (...args) => openCodeEnvRuntime.resolveOpencodeCliPath(...args);
|
||||
const isBundledOpenCodeCliPath = (...args) => openCodeEnvRuntime.isBundledOpenCodeCliPath(...args);
|
||||
const isExecutable = (...args) => openCodeEnvRuntime.isExecutable(...args);
|
||||
const searchPathFor = (...args) => openCodeEnvRuntime.searchPathFor(...args);
|
||||
const resolveGitBinaryForSpawn = (...args) => openCodeEnvRuntime.resolveGitBinaryForSpawn(...args);
|
||||
@@ -1070,6 +1072,18 @@ const openCodeLifecycleRuntime = createOpenCodeLifecycleRuntime({
|
||||
},
|
||||
});
|
||||
|
||||
const getOpenCodeUpgradeCapability = () => {
|
||||
const activeBinary = lastOpenCodeLaunchDiagnostics?.sourceBinary
|
||||
|| lastOpenCodeLaunchDiagnostics?.binary
|
||||
|| resolvedOpencodeBinary;
|
||||
return resolveOpenCodeUpgradeCapability({
|
||||
isExternal: isExternalOpenCode,
|
||||
hasManagedProcess: Boolean(openCodeProcess),
|
||||
activeBinary,
|
||||
isBundledBinary: isBundledOpenCodeCliPath,
|
||||
});
|
||||
};
|
||||
|
||||
const restartOpenCode = (...args) => openCodeLifecycleRuntime.restartOpenCode(...args);
|
||||
const waitForOpenCodeReady = (...args) => openCodeLifecycleRuntime.waitForOpenCodeReady(...args);
|
||||
const waitForAgentPresence = (...args) => openCodeLifecycleRuntime.waitForAgentPresence(...args);
|
||||
@@ -1600,6 +1614,7 @@ async function main(options = {}) {
|
||||
readCustomThemesFromDisk,
|
||||
refreshOpenCodeAfterConfigChange,
|
||||
getOpenCodeResolutionSnapshot,
|
||||
getOpenCodeUpgradeCapability,
|
||||
formatSettingsResponse,
|
||||
readSettingsFromDisk,
|
||||
readSettingsFromDiskMigrated,
|
||||
|
||||
@@ -26,6 +26,7 @@ This module provides OpenCode server integration utilities for the web server ru
|
||||
- `packages/web/server/lib/opencode/static-routes-runtime.js`: static asset/SPA fallback route registration and manifest route wiring.
|
||||
- `packages/web/server/lib/opencode/feature-routes-runtime.js`: feature route composition runtime for dynamic import-backed config/skill/provider route registration.
|
||||
- `packages/web/server/lib/opencode/opencode-resolution-runtime.js`: OpenCode binary resolution snapshot runtime for settings routes and diagnostics.
|
||||
- `packages/web/server/lib/opencode/upgrade-capability.js`: authoritative upgrade ownership policy for the active OpenCode runtime. Bundled, external, and unresolved runtimes fail closed; only managed non-bundled runtimes delegate upgrades to OpenCode.
|
||||
- `packages/web/server/lib/opencode/tunnel-wiring-runtime.js`: tunnel service/routes composition runtime and active-port wiring for main server startup.
|
||||
- `packages/web/server/lib/opencode/startup-pipeline-runtime.js`: server startup tail orchestration runtime for terminal/proxy/static/start-listen flow.
|
||||
- `packages/web/server/lib/agent-tool/runtime.js`: managed OpenCode custom-tool materialization, environment injection, loopback authentication, and fixed CLI action dispatch.
|
||||
@@ -76,8 +77,8 @@ This module provides OpenCode server integration utilities for the web server ru
|
||||
- `GET /api/config/settings`
|
||||
- `PUT /api/config/settings`
|
||||
- `GET /api/config/opencode-resolution`
|
||||
- `POST /api/opencode/upgrade` (proxies OpenCode upgrade, then restarts managed OpenCode so the new binary is active)
|
||||
- `GET /api/opencode/upgrade-status`
|
||||
- `POST /api/opencode/upgrade` (enforces the active runtime's upgrade capability, serializes supported OpenCode upgrades, then restarts managed OpenCode so the new binary is active)
|
||||
- `GET /api/opencode/upgrade-status` (returns version availability plus the authoritative `upgrade.supported`, `upgrade.manager`, and `upgrade.reason` capability)
|
||||
- `POST /api/opencode/directory`
|
||||
- `GET /api/provider/:providerId/source`
|
||||
- `DELETE /api/provider/:providerId/auth`
|
||||
|
||||
@@ -301,6 +301,23 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const canonicalExecutablePath = (candidate) => {
|
||||
if (typeof candidate !== 'string' || !candidate.trim()) return null;
|
||||
try {
|
||||
return fs.realpathSync.native(candidate.trim());
|
||||
} catch {
|
||||
return path.resolve(candidate.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const isBundledOpenCodeCliPath = (candidate) => {
|
||||
const canonicalCandidate = canonicalExecutablePath(candidate);
|
||||
if (!canonicalCandidate) return false;
|
||||
return bundledOpenCodeCliCandidates().some((bundledCandidate) => (
|
||||
canonicalExecutablePath(bundledCandidate) === canonicalCandidate
|
||||
));
|
||||
};
|
||||
|
||||
const bundledOpenCodeCliFallback = () => {
|
||||
const bundled = resolveBundledOpenCodeCliPath();
|
||||
if (!bundled) return null;
|
||||
@@ -1164,6 +1181,7 @@ export const createOpenCodeEnvRuntime = (deps) => {
|
||||
applyOpencodeBinaryFromSettings,
|
||||
getLoginShellEnvSnapshot,
|
||||
resolveOpencodeCliPath,
|
||||
isBundledOpenCodeCliPath,
|
||||
resolveManagedOpenCodeLaunchSpec,
|
||||
isExecutable,
|
||||
searchPathFor,
|
||||
|
||||
@@ -183,6 +183,18 @@ describe('OpenCode env runtime', () => {
|
||||
expect(state.resolvedOpencodeBinarySource).toBe('bundled');
|
||||
});
|
||||
|
||||
it('recognizes the bundled CLI by canonical path', () => {
|
||||
const bundledDir = createTempDir('openchamber-bundled-opencode-');
|
||||
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
||||
fs.writeFileSync(bundledBinary, '#!/bin/sh\nexit 0\n');
|
||||
if (process.platform !== 'win32') fs.chmodSync(bundledBinary, 0o755);
|
||||
process.env.OPENCHAMBER_BUNDLED_OPENCODE_CLI_DIR = bundledDir;
|
||||
const { runtime } = createRuntime({});
|
||||
|
||||
expect(runtime.isBundledOpenCodeCliPath(bundledBinary)).toBe(true);
|
||||
expect(runtime.isBundledOpenCodeCliPath(path.join(bundledDir, 'other'))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps explicit OpenCode binary ahead of bundled CLI', () => {
|
||||
const bundledDir = createTempDir('openchamber-bundled-opencode-');
|
||||
const bundledBinary = path.join(bundledDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
|
||||
|
||||
@@ -86,6 +86,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
readCustomThemesFromDisk,
|
||||
refreshOpenCodeAfterConfigChange,
|
||||
getOpenCodeResolutionSnapshot,
|
||||
getOpenCodeUpgradeCapability,
|
||||
formatSettingsResponse,
|
||||
readSettingsFromDisk,
|
||||
readSettingsFromDiskMigrated,
|
||||
@@ -121,6 +122,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
|
||||
crypto,
|
||||
clientReloadDelayMs,
|
||||
getOpenCodeResolutionSnapshot,
|
||||
getOpenCodeUpgradeCapability,
|
||||
formatSettingsResponse,
|
||||
readSettingsFromDisk,
|
||||
readSettingsFromDiskMigrated,
|
||||
|
||||
@@ -241,6 +241,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
||||
|
||||
const createManagedOpenCodeServerProcess = async ({ hostname, port, timeout, cwd, env: processEnv, shellEnvKeysCount = 0 }) => {
|
||||
let binary = (process.env.OPENCODE_BINARY || 'opencode').trim() || 'opencode';
|
||||
const sourceBinary = binary;
|
||||
let args = ['serve', '--hostname', hostname, '--port', String(port)];
|
||||
let launchWrapperType = null;
|
||||
|
||||
@@ -264,6 +265,7 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
||||
const pathEntryCount = pathValue ? pathValue.split(process.platform === 'win32' ? ';' : ':').filter(Boolean).length : 0;
|
||||
state.lastOpenCodeLaunchDiagnostics = {
|
||||
launchedAt: new Date().toISOString(),
|
||||
sourceBinary,
|
||||
binary,
|
||||
args,
|
||||
cwd,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import { registerOpenCodeRoutes } from './routes.js';
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
const createApp = (overrides = {}) => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
const dependencies = {
|
||||
getOpenCodeUpgradeCapability: () => ({
|
||||
supported: false,
|
||||
manager: 'openchamber',
|
||||
reason: 'bundled',
|
||||
}),
|
||||
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
||||
getOpenCodeAuthHeaders: () => ({}),
|
||||
refreshOpenCodeAfterConfigChange: vi.fn(async () => {}),
|
||||
...overrides,
|
||||
};
|
||||
registerOpenCodeRoutes(app, dependencies);
|
||||
return { app, dependencies };
|
||||
};
|
||||
|
||||
describe('OpenCode upgrade routes', () => {
|
||||
it('fails closed without contacting the bundled OpenCode updater', async () => {
|
||||
globalThis.fetch = vi.fn();
|
||||
const { app } = createApp();
|
||||
|
||||
await request(app)
|
||||
.post('/api/opencode/upgrade')
|
||||
.send({})
|
||||
.expect(409, {
|
||||
success: false,
|
||||
code: 'OPENCODE_UPGRADE_MANAGED_BY_OPENCHAMBER',
|
||||
error: 'OpenCode is bundled with OpenChamber Desktop and updates with the app.',
|
||||
});
|
||||
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports bundled update ownership through the capability contract', async () => {
|
||||
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify({ healthy: true, version: '1.18.8' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
const { app } = createApp();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/opencode/upgrade-status')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
available: false,
|
||||
currentVersion: '1.18.8',
|
||||
latestVersion: null,
|
||||
upgrade: {
|
||||
supported: false,
|
||||
manager: 'openchamber',
|
||||
reason: 'bundled',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('serializes supported upgrades and preserves the in-flight lock', async () => {
|
||||
let releaseUpgrade;
|
||||
const upstreamResponse = new Promise((resolve) => {
|
||||
releaseUpgrade = () => resolve(new Response(JSON.stringify({ success: true, version: '1.18.9' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
});
|
||||
globalThis.fetch = vi.fn(() => upstreamResponse);
|
||||
const { app, dependencies } = createApp({
|
||||
getOpenCodeUpgradeCapability: () => ({
|
||||
supported: true,
|
||||
manager: 'opencode',
|
||||
reason: null,
|
||||
}),
|
||||
});
|
||||
|
||||
const first = request(app)
|
||||
.post('/api/opencode/upgrade')
|
||||
.send({})
|
||||
.expect(200, {
|
||||
success: true,
|
||||
version: '1.18.9',
|
||||
restarted: true,
|
||||
})
|
||||
.then((response) => response);
|
||||
await vi.waitFor(() => {
|
||||
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
await request(app)
|
||||
.post('/api/opencode/upgrade')
|
||||
.send({})
|
||||
.expect(409, {
|
||||
success: false,
|
||||
code: 'OPENCODE_UPGRADE_IN_PROGRESS',
|
||||
error: 'An OpenCode upgrade is already in progress.',
|
||||
});
|
||||
|
||||
releaseUpgrade();
|
||||
await first;
|
||||
expect(dependencies.refreshOpenCodeAfterConfigChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
crypto,
|
||||
clientReloadDelayMs,
|
||||
getOpenCodeResolutionSnapshot,
|
||||
getOpenCodeUpgradeCapability,
|
||||
formatSettingsResponse,
|
||||
readSettingsFromDisk,
|
||||
readSettingsFromDiskMigrated,
|
||||
@@ -41,12 +42,6 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
return trimmed || null;
|
||||
};
|
||||
|
||||
const isBundledOpenCodeBinaryActive = async () => {
|
||||
const settings = await readSettingsFromDiskMigrated();
|
||||
const resolution = await getOpenCodeResolutionSnapshot(settings);
|
||||
return resolution?.source === 'bundled' || resolution?.detectedSourceNow === 'bundled';
|
||||
};
|
||||
|
||||
const readOpenCodeCurrentVersion = async () => {
|
||||
const healthResponse = await fetch(buildOpenCodeUrl('/global/health', ''), {
|
||||
method: 'GET',
|
||||
@@ -153,48 +148,84 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
}
|
||||
});
|
||||
|
||||
let openCodeUpgradePromise = null;
|
||||
|
||||
app.post('/api/opencode/upgrade', async (req, res) => {
|
||||
try {
|
||||
if (await isBundledOpenCodeBinaryActive()) {
|
||||
const capability = getOpenCodeUpgradeCapability();
|
||||
if (!capability.supported) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: 'OpenCode is bundled with OpenChamber Desktop and cannot be upgraded separately.',
|
||||
code: capability.reason === 'bundled'
|
||||
? 'OPENCODE_UPGRADE_MANAGED_BY_OPENCHAMBER'
|
||||
: 'OPENCODE_UPGRADE_UNSUPPORTED',
|
||||
error: capability.reason === 'bundled'
|
||||
? 'OpenCode is bundled with OpenChamber Desktop and updates with the app.'
|
||||
: 'This OpenCode runtime cannot be upgraded by OpenChamber.',
|
||||
});
|
||||
}
|
||||
if (openCodeUpgradePromise) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
code: 'OPENCODE_UPGRADE_IN_PROGRESS',
|
||||
error: 'An OpenCode upgrade is already in progress.',
|
||||
});
|
||||
}
|
||||
|
||||
const target = typeof req.body?.target === 'string' && req.body.target.trim().length > 0
|
||||
? req.body.target.trim()
|
||||
: undefined;
|
||||
const response = await fetch(buildOpenCodeUrl('/global/upgrade', ''), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...getOpenCodeAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify(target ? { target } : {}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
return res.status(response.status).json({
|
||||
success: false,
|
||||
error: payload?.error || response.statusText || 'Failed to upgrade OpenCode',
|
||||
const upgradeOperation = (async () => {
|
||||
const response = await fetch(buildOpenCodeUrl('/global/upgrade', ''), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...getOpenCodeAuthHeaders(),
|
||||
},
|
||||
body: JSON.stringify(target ? { target } : {}),
|
||||
});
|
||||
}
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: response.status,
|
||||
body: {
|
||||
success: false,
|
||||
error: payload?.error || response.statusText || 'Failed to upgrade OpenCode',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshOpenCodeAfterConfigChange('OpenCode upgrade');
|
||||
} catch (restartError) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
success: false,
|
||||
upgraded: true,
|
||||
error: restartError instanceof Error
|
||||
? `OpenCode upgraded, but restart failed: ${restartError.message}`
|
||||
: 'OpenCode upgraded, but restart failed',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: { ...(payload ?? { success: true }), restarted: true },
|
||||
};
|
||||
})();
|
||||
openCodeUpgradePromise = upgradeOperation;
|
||||
|
||||
try {
|
||||
await refreshOpenCodeAfterConfigChange('OpenCode upgrade');
|
||||
} catch (restartError) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
upgraded: true,
|
||||
error: restartError instanceof Error
|
||||
? `OpenCode upgraded, but restart failed: ${restartError.message}`
|
||||
: 'OpenCode upgraded, but restart failed',
|
||||
});
|
||||
const result = await upgradeOperation;
|
||||
return res.status(result.status).json(result.body);
|
||||
} finally {
|
||||
if (openCodeUpgradePromise === upgradeOperation) {
|
||||
openCodeUpgradePromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ ...(payload ?? { success: true }), restarted: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to upgrade OpenCode:', error);
|
||||
return res.status(500).json({
|
||||
@@ -206,13 +237,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
|
||||
app.get('/api/opencode/upgrade-status', async (_req, res) => {
|
||||
try {
|
||||
if (await isBundledOpenCodeBinaryActive()) {
|
||||
const capability = getOpenCodeUpgradeCapability();
|
||||
if (!capability.supported) {
|
||||
const current = await readOpenCodeCurrentVersion().catch(() => ({ ok: false, currentVersion: null }));
|
||||
return res.json({
|
||||
available: false,
|
||||
currentVersion: current.ok ? current.currentVersion : null,
|
||||
latestVersion: null,
|
||||
source: 'bundled',
|
||||
upgrade: capability,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,6 +271,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
available,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
upgrade: capability,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export const resolveOpenCodeUpgradeCapability = ({
|
||||
isExternal,
|
||||
hasManagedProcess,
|
||||
activeBinary,
|
||||
isBundledBinary,
|
||||
}) => {
|
||||
if (isExternal) {
|
||||
return {
|
||||
supported: false,
|
||||
manager: 'external',
|
||||
reason: 'external',
|
||||
};
|
||||
}
|
||||
|
||||
if (!hasManagedProcess || !activeBinary) {
|
||||
return {
|
||||
supported: false,
|
||||
manager: null,
|
||||
reason: 'unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
if (isBundledBinary(activeBinary)) {
|
||||
return {
|
||||
supported: false,
|
||||
manager: 'openchamber',
|
||||
reason: 'bundled',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
supported: true,
|
||||
manager: 'opencode',
|
||||
reason: null,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { resolveOpenCodeUpgradeCapability } from './upgrade-capability.js';
|
||||
|
||||
describe('OpenCode upgrade capability', () => {
|
||||
it('assigns bundled binaries to the OpenChamber updater', () => {
|
||||
const isBundledBinary = vi.fn(() => true);
|
||||
|
||||
expect(resolveOpenCodeUpgradeCapability({
|
||||
isExternal: false,
|
||||
hasManagedProcess: true,
|
||||
activeBinary: '/Applications/OpenChamber.app/Contents/Resources/opencode-cli/opencode',
|
||||
isBundledBinary,
|
||||
})).toEqual({
|
||||
supported: false,
|
||||
manager: 'openchamber',
|
||||
reason: 'bundled',
|
||||
});
|
||||
});
|
||||
|
||||
it('never upgrades external or unresolved runtimes', () => {
|
||||
const isBundledBinary = vi.fn(() => false);
|
||||
|
||||
expect(resolveOpenCodeUpgradeCapability({
|
||||
isExternal: true,
|
||||
hasManagedProcess: false,
|
||||
activeBinary: null,
|
||||
isBundledBinary,
|
||||
})).toEqual({
|
||||
supported: false,
|
||||
manager: 'external',
|
||||
reason: 'external',
|
||||
});
|
||||
expect(resolveOpenCodeUpgradeCapability({
|
||||
isExternal: false,
|
||||
hasManagedProcess: false,
|
||||
activeBinary: '/usr/local/bin/opencode',
|
||||
isBundledBinary,
|
||||
})).toEqual({
|
||||
supported: false,
|
||||
manager: null,
|
||||
reason: 'unavailable',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows OpenCode to upgrade a managed non-bundled binary', () => {
|
||||
expect(resolveOpenCodeUpgradeCapability({
|
||||
isExternal: false,
|
||||
hasManagedProcess: true,
|
||||
activeBinary: '/Users/alice/.opencode/bin/opencode',
|
||||
isBundledBinary: () => false,
|
||||
})).toEqual({
|
||||
supported: true,
|
||||
manager: 'opencode',
|
||||
reason: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user