From 8757f9ac6fcdedd69da27a40092dcd1636ba1c09 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Thu, 16 Apr 2026 15:26:39 +0300 Subject: [PATCH] fix: defer outdated notification until state machine completes Delay the outdated notification until after initial setup so it does not fire during an in-progress update. Add staleness checks to the update command so it verifies the workspace is still outdated before proceeding. --- src/commands.ts | 18 ++++++++++++ src/workspace/workspaceMonitor.ts | 10 +++++-- test/unit/workspace/workspaceMonitor.test.ts | 30 ++++++++++++++++---- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index b8a08aa5..3d7d51ea 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -761,6 +761,15 @@ export class Commands { if (!this.workspace || !this.remoteWorkspaceClient) { return; } + const showUpToDate = () => + vscode.window.showInformationMessage( + "The workspace is already up to date.", + ); + + if (!this.workspace.outdated) { + showUpToDate(); + return; + } const action = await vscodeProposed.window.showWarningMessage( "Update Workspace", { @@ -774,6 +783,15 @@ export class Commands { return; } + // Re-check; workspace may have been updated or disconnected while the modal was open. + if (!this.workspace) { + return; + } + if (!this.workspace.outdated) { + showUpToDate(); + return; + } + this.logger.info( `Updating workspace ${createWorkspaceIdentifier(this.workspace)}`, ); diff --git a/src/workspace/workspaceMonitor.ts b/src/workspace/workspaceMonitor.ts index e353e1f8..7f37b07a 100644 --- a/src/workspace/workspaceMonitor.ts +++ b/src/workspace/workspaceMonitor.ts @@ -43,6 +43,8 @@ export class WorkspaceMonitor implements vscode.Disposable { // For logging. private readonly name: string; + private latestWorkspace: Workspace; + private constructor( workspace: Workspace, private readonly client: CoderApi, @@ -50,6 +52,7 @@ export class WorkspaceMonitor implements vscode.Disposable { private readonly contextManager: ContextManager, ) { this.name = createWorkspaceIdentifier(workspace); + this.latestWorkspace = workspace; const statusBarItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, @@ -115,6 +118,7 @@ export class WorkspaceMonitor implements vscode.Disposable { public markInitialSetupComplete(): void { this.completedInitialSetup = true; + this.maybeNotify(this.latestWorkspace); } /** @@ -130,6 +134,7 @@ export class WorkspaceMonitor implements vscode.Disposable { } private update(workspace: Workspace) { + this.latestWorkspace = workspace; this.updateContext(workspace); this.updateStatusBar(workspace); } @@ -139,10 +144,9 @@ export class WorkspaceMonitor implements vscode.Disposable { if (areNotificationsDisabled(cfg)) { return; } - this.maybeNotifyOutdated(workspace, cfg); this.maybeNotifyAutostop(workspace); if (this.completedInitialSetup) { - // This instance might be created before the workspace is running + this.maybeNotifyOutdated(workspace, cfg); this.maybeNotifyDeletion(workspace); this.maybeNotifyNotRunning(workspace); } @@ -239,7 +243,7 @@ export class WorkspaceMonitor implements vscode.Disposable { if (action === "Update") { vscode.commands.executeCommand( "coder.workspace.update", - workspace, + this.latestWorkspace, this.client, ); } diff --git a/test/unit/workspace/workspaceMonitor.test.ts b/test/unit/workspace/workspaceMonitor.test.ts index 6a6742b4..3373bbe7 100644 --- a/test/unit/workspace/workspaceMonitor.test.ts +++ b/test/unit/workspace/workspaceMonitor.test.ts @@ -153,9 +153,10 @@ describe("WorkspaceMonitor", () => { ); }); - it("does not show deletion or not-running notifications before initial setup", async () => { + it("does not show deletion, outdated, or not-running notifications before initial setup", async () => { const { stream } = await setup(); + stream.pushMessage(workspaceEvent({ outdated: true })); stream.pushMessage( workspaceEvent({ deleting_at: minutesFromNow(12 * 60) }), ); @@ -167,7 +168,8 @@ describe("WorkspaceMonitor", () => { }); it("fetches template details for outdated notification", async () => { - const { stream } = await setup(); + const { monitor, stream } = await setup(); + monitor.markInitialSetupComplete(); stream.pushMessage(workspaceEvent({ outdated: true })); @@ -179,6 +181,22 @@ describe("WorkspaceMonitor", () => { }); }); + it("fires outdated notification on markInitialSetupComplete", async () => { + const { monitor, stream } = await setup(); + + stream.pushMessage(workspaceEvent({ outdated: true })); + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); + + monitor.markInitialSetupComplete(); + + await vi.waitFor(() => { + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith( + expect.stringContaining("template v2"), + "Update", + ); + }); + }); + it("only notifies once per event type", async () => { const { stream } = await setup(); @@ -197,11 +215,12 @@ describe("WorkspaceMonitor", () => { describe("disableUpdateNotifications", () => { it("suppresses outdated notification but allows other types", async () => { - const { stream, client, config } = await setup(); + const { monitor, stream, config } = await setup(); + monitor.markInitialSetupComplete(); config.set("coder.disableUpdateNotifications", true); stream.pushMessage(workspaceEvent({ outdated: true })); - expect(client.getTemplate).not.toHaveBeenCalled(); + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); stream.pushMessage( workspaceEvent({ @@ -217,7 +236,8 @@ describe("WorkspaceMonitor", () => { }); it("shows outdated notification after re-enabling", async () => { - const { stream, config } = await setup(); + const { monitor, stream, config } = await setup(); + monitor.markInitialSetupComplete(); config.set("coder.disableUpdateNotifications", true); stream.pushMessage(workspaceEvent({ outdated: true }));