Skip to content

Commit 36bf687

Browse files
parsakhazrunpane
authored andcommitted
fix: fall back to hosted daemon state
1 parent 0610a0e commit 36bf687

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

main/src/services/cloudVmManager.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ describe('CloudVmManager', () => {
102102
provider: 'gcp',
103103
apiToken: 'secret-token',
104104
serverId: 'pane-user123',
105+
projectId: 'pane-project',
106+
zone: 'us-central1-a',
105107
vncPassword: 'vnc-password',
106108
tunnelPort: 8080,
107109
daemonStatus: 'ready',
@@ -159,4 +161,59 @@ describe('CloudVmManager', () => {
159161
error: null,
160162
});
161163
});
164+
165+
it('surfaces daemon-backed hosted workspace state when lifecycle fields are incomplete', async () => {
166+
const manager = new CloudVmManager(new ConfigManagerStub(normalizeCloudVmConfig({
167+
provider: 'gcp',
168+
apiToken: 'stale-token',
169+
serverId: 'pane-user123',
170+
daemonStatus: 'ready',
171+
daemonBaseUrl: 'https://pane.example.com/daemon/',
172+
linkedRemoteProfileId: 'remote-profile-1',
173+
})) as never);
174+
175+
const fetchVmStatusSpy = vi.spyOn(manager as never, 'fetchVmStatus');
176+
177+
const state = await manager.getState();
178+
179+
expect(fetchVmStatusSpy).not.toHaveBeenCalled();
180+
expect(state).toMatchObject({
181+
status: 'running',
182+
provider: 'gcp',
183+
serverId: 'pane-user123',
184+
daemonStatus: 'ready',
185+
daemonBaseUrl: 'https://pane.example.com/daemon/',
186+
linkedRemoteProfileId: 'remote-profile-1',
187+
tunnelStatus: 'off',
188+
error: null,
189+
});
190+
});
191+
192+
it('falls back to daemon-backed hosted workspace state when lifecycle status fetch fails', async () => {
193+
const manager = new CloudVmManager(new ConfigManagerStub(normalizeCloudVmConfig({
194+
provider: 'gcp',
195+
apiToken: 'stale-token',
196+
serverId: 'pane-user123',
197+
projectId: 'pane-project',
198+
zone: 'us-central1-a',
199+
daemonStatus: 'ready',
200+
daemonBaseUrl: 'https://pane.example.com/daemon/',
201+
linkedRemoteProfileId: 'remote-profile-1',
202+
})) as never);
203+
204+
vi.spyOn(manager as never, 'fetchVmStatus').mockRejectedValue(new Error('401 Unauthorized'));
205+
206+
const state = await manager.getState();
207+
208+
expect(state).toMatchObject({
209+
status: 'running',
210+
provider: 'gcp',
211+
serverId: 'pane-user123',
212+
daemonStatus: 'ready',
213+
daemonBaseUrl: 'https://pane.example.com/daemon/',
214+
linkedRemoteProfileId: 'remote-profile-1',
215+
tunnelStatus: 'off',
216+
error: null,
217+
});
218+
});
162219
});

main/src/services/cloudVmManager.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ export class CloudVmManager extends EventEmitter {
111111
} catch (err) {
112112
const message = err instanceof Error ? err.message : String(err);
113113
this.logger?.error(`[CloudVM] Failed to fetch status: ${message}`);
114+
115+
if (this.hasDaemonHostedWorkspaceState(config)) {
116+
this.cachedState = {
117+
status: this.mapDaemonStatusToVmStatus(config.daemonStatus ?? 'unknown'),
118+
ip: null,
119+
noVncUrl: null,
120+
...this.getHostedWorkspaceStateFromConfig(config),
121+
lastChecked: new Date().toISOString(),
122+
error: null,
123+
tunnelStatus: this.tunnelStatus,
124+
};
125+
return { ...this.cachedState };
126+
}
127+
114128
this.cachedState.error = message;
115129
this.cachedState.status = 'unknown';
116130
}
@@ -597,7 +611,12 @@ export class CloudVmManager extends EventEmitter {
597611
}
598612

599613
private canManageVmLifecycle(config: CloudVmConfig): boolean {
600-
return config.apiToken.trim().length > 0;
614+
return Boolean(
615+
config.apiToken.trim().length > 0
616+
&& config.serverId
617+
&& config.projectId
618+
&& config.zone,
619+
);
601620
}
602621

603622
private hasDaemonHostedWorkspaceState(config: CloudVmConfig): boolean {

0 commit comments

Comments
 (0)