fix(updater): verify native host version and report restart failures
Complete #3227 by keeping browser completion polls on the native updater, checking the requested version, and preserving retry access after a failed restart.
This commit is contained in:
@@ -368,6 +368,16 @@ before starting managed OpenCode. The managed custom tool therefore receives
|
||||
an authoritative loopback callback URL even when OpenChamber binds port `0`.
|
||||
|
||||
## Public exports (openchamber-routes.js)
|
||||
Browser completion checks use `appType=web&updateStatus=true` to stay on the
|
||||
Desktop Host's native updater. A rejected native restart is retained in the
|
||||
server process and returned to these polls as `DESKTOP_UPDATE_RESTART_FAILED`;
|
||||
ordinary availability checks remain usable so a browser reload can offer a
|
||||
retry. Starting another installation clears the previous restart error.
|
||||
The shared UI's `lib/web-update.ts` parses install/check responses and waits
|
||||
for the installed native target version, rather than treating absence of a
|
||||
newer release as installation success. Poll requests have individual deadlines
|
||||
within a ten-minute overall deadline.
|
||||
|
||||
- `registerOpenChamberRoutes(app, dependencies)`: registers OpenChamber endpoints:
|
||||
- `GET /api/openchamber/update-check`
|
||||
- `POST /api/openchamber/update-install`
|
||||
|
||||
@@ -32,6 +32,8 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
desktopUpdater,
|
||||
} = dependencies;
|
||||
|
||||
let desktopRestartError = null;
|
||||
|
||||
app.get('/api/openchamber/update-check', async (req, res) => {
|
||||
try {
|
||||
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
||||
@@ -61,6 +63,12 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
};
|
||||
let updateInfo;
|
||||
if (process.env.OPENCHAMBER_RUNTIME === 'desktop' && updateRequest.appType === 'web') {
|
||||
if (desktopRestartError && req.query.updateStatus === 'true') {
|
||||
return res.status(503).json({
|
||||
code: 'DESKTOP_UPDATE_RESTART_FAILED',
|
||||
error: desktopRestartError,
|
||||
});
|
||||
}
|
||||
if (typeof desktopUpdater?.check !== 'function') {
|
||||
return res.status(503).json({
|
||||
available: false,
|
||||
@@ -97,6 +105,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
});
|
||||
}
|
||||
|
||||
desktopRestartError = null;
|
||||
const updateInfo = await desktopUpdater.install();
|
||||
if (!updateInfo?.available) {
|
||||
return res.status(400).json({ error: 'No update available' });
|
||||
@@ -115,7 +124,10 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
|
||||
setImmediate(() => {
|
||||
Promise.resolve()
|
||||
.then(() => desktopUpdater.restart())
|
||||
.catch((error) => console.error('Failed to restart after desktop update:', error));
|
||||
.catch((error) => {
|
||||
desktopRestartError = error instanceof Error ? error.message : 'Failed to restart after desktop update';
|
||||
console.error('Failed to restart after desktop update:', error);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,43 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('OpenChamber desktop host update route', () => {
|
||||
it('reports a restart rejection until the user retries installation', async () => {
|
||||
const desktopUpdater = {
|
||||
check: vi.fn(async () => ({ available: true, currentVersion: '1.17.0', version: '1.17.1' })),
|
||||
install: vi.fn(async () => ({ available: true, version: '1.17.1' })),
|
||||
restart: vi.fn().mockRejectedValueOnce(new Error('Signature rejected')).mockResolvedValue(undefined),
|
||||
};
|
||||
const { app } = createApp({ environment: { OPENCHAMBER_RUNTIME: 'desktop' }, desktopUpdater });
|
||||
const logError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await request(app).post('/api/openchamber/update-install').expect(200);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false&updateStatus=true').expect(503, {
|
||||
code: 'DESKTOP_UPDATE_RESTART_FAILED', error: 'Signature rejected',
|
||||
});
|
||||
expect(desktopUpdater.check).not.toHaveBeenCalled();
|
||||
expect(logError).toHaveBeenCalledOnce();
|
||||
// Availability remains reachable after a browser reload, so users can retry.
|
||||
await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
||||
|
||||
await request(app).post('/api/openchamber/update-install').expect(200);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const response = await request(app).get('/api/openchamber/update-check?appType=web&reportUsage=false').expect(200);
|
||||
expect(response.body.currentVersion).toBe('1.17.0');
|
||||
expect(response.body.updateOwner).toBe('electron-updater');
|
||||
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects native checks without a bridge and preserves explicit non-web checks', async () => {
|
||||
const { app } = createApp({ environment: { OPENCHAMBER_RUNTIME: 'desktop' } });
|
||||
await request(app).get('/api/openchamber/update-check?appType=web').expect(503, {
|
||||
available: false, code: 'DESKTOP_UPDATER_UNAVAILABLE', error: 'The desktop updater is not available.',
|
||||
});
|
||||
expect(packageManager.checkForUpdates).not.toHaveBeenCalled();
|
||||
await request(app).get('/api/openchamber/update-check?appType=desktop-electron').expect(200);
|
||||
expect(packageManager.checkForUpdates).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('uses electron-updater to check for Web client updates', async () => {
|
||||
const desktopUpdater = {
|
||||
check: vi.fn(async () => ({
|
||||
|
||||
Reference in New Issue
Block a user