Skip to content

Commit 684cfd0

Browse files
committed
Use task pause/resume API instead of workspace stop/start
Use the dedicated pauseTask/resumeTask endpoints which set the correct build_reason for log snapshot capture and task lifecycle tracking. Falls back to stopWorkspace/startWorkspace on 404 for older Coder servers.
1 parent ea84a40 commit 684cfd0

File tree

2 files changed

+139
-46
lines changed

2 files changed

+139
-46
lines changed

src/webviews/tasks/tasksPanelProvider.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class TasksPanelProvider
8888

8989
private view?: vscode.WebviewView;
9090
private disposables: vscode.Disposable[] = [];
91+
private useLegacyPauseResume = false;
9192

9293
// Workspace log streaming
9394
private readonly buildLogStream = new LazyStream<ProvisionerJobLog>();
@@ -285,28 +286,50 @@ export class TasksPanelProvider
285286
);
286287
}
287288

288-
private async handlePauseTask(taskId: string): Promise<void> {
289-
const task = await this.client.getTask("me", taskId);
290-
if (!task.workspace_id) {
291-
throw new Error("Task has no workspace");
292-
}
293-
294-
await this.client.stopWorkspace(task.workspace_id);
289+
private handlePauseTask(taskId: string): Promise<void> {
290+
return this.pauseOrResumeTask(
291+
taskId,
292+
() => this.client.pauseTask("me", taskId),
293+
(task) => this.client.stopWorkspace(task.workspace_id!),
294+
);
295+
}
295296

296-
await this.refreshAndNotifyTask(taskId);
297+
private handleResumeTask(taskId: string): Promise<void> {
298+
return this.pauseOrResumeTask(
299+
taskId,
300+
() => this.client.resumeTask("me", taskId),
301+
(task) =>
302+
this.client.startWorkspace(
303+
task.workspace_id!,
304+
task.template_version_id,
305+
),
306+
);
297307
}
298308

299-
private async handleResumeTask(taskId: string): Promise<void> {
309+
private async pauseOrResumeTask(
310+
taskId: string,
311+
taskApiCall: () => Promise<unknown>,
312+
legacyCall: (task: Task) => Promise<unknown>,
313+
): Promise<void> {
314+
if (!this.useLegacyPauseResume) {
315+
try {
316+
await taskApiCall();
317+
await this.refreshAndNotifyTask(taskId);
318+
return;
319+
} catch (err) {
320+
if (isAxiosError(err) && err.response?.status === 404) {
321+
this.useLegacyPauseResume = true;
322+
} else {
323+
throw err;
324+
}
325+
}
326+
}
327+
300328
const task = await this.client.getTask("me", taskId);
301329
if (!task.workspace_id) {
302330
throw new Error("Task has no workspace");
303331
}
304-
305-
await this.client.startWorkspace(
306-
task.workspace_id,
307-
task.template_version_id,
308-
);
309-
332+
await legacyCall(task);
310333
await this.refreshAndNotifyTask(taskId);
311334
}
312335

test/unit/webviews/tasks/tasksPanelProvider.test.ts

Lines changed: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ type TasksPanelClient = Pick<
7373
| "getTemplateVersionPresets"
7474
| "startWorkspace"
7575
| "stopWorkspace"
76+
| "pauseTask"
77+
| "resumeTask"
7678
| "sendTaskInput"
7779
| "getHost"
7880
| "getWorkspace"
@@ -91,6 +93,8 @@ function createClient(baseUrl = "https://coder.example.com"): MockClient {
9193
getTemplateVersionPresets: vi.fn().mockResolvedValue([]),
9294
startWorkspace: vi.fn().mockResolvedValue(undefined),
9395
stopWorkspace: vi.fn().mockResolvedValue(undefined),
96+
pauseTask: vi.fn().mockResolvedValue(undefined),
97+
resumeTask: vi.fn().mockResolvedValue(undefined),
9498
sendTaskInput: vi.fn().mockResolvedValue(undefined),
9599
getHost: vi.fn().mockReturnValue(baseUrl),
96100
getWorkspace: vi.fn().mockResolvedValue(workspace()),
@@ -413,40 +417,104 @@ describe("TasksPanelProvider", () => {
413417
});
414418

415419
describe("pauseTask / resumeTask", () => {
416-
interface WorkspaceControlTestCase {
417-
method: typeof TasksApi.pauseTask;
418-
clientMethod: keyof MockClient;
419-
taskOverrides: Partial<Task>;
420-
}
421-
it.each<WorkspaceControlTestCase>([
422-
{
423-
method: TasksApi.pauseTask,
424-
clientMethod: "stopWorkspace",
425-
taskOverrides: { workspace_id: "ws-1" },
426-
},
427-
{
428-
method: TasksApi.resumeTask,
429-
clientMethod: "startWorkspace",
430-
taskOverrides: { workspace_id: "ws-1", template_version_id: "tv-1" },
431-
},
432-
])(
433-
"$method.method calls $clientMethod",
434-
async ({ method, clientMethod, taskOverrides }) => {
435-
const h = createHarness();
436-
h.client.getTask.mockResolvedValue(task(taskOverrides));
420+
it("pauseTask calls client.pauseTask", async () => {
421+
const h = createHarness();
422+
h.client.getTask.mockResolvedValue(task({ workspace_id: "ws-1" }));
437423

438-
const res = await h.request(method, {
439-
taskId: "task-1",
440-
taskName: "Test Task",
441-
});
424+
const res = await h.request(TasksApi.pauseTask, {
425+
taskId: "task-1",
426+
taskName: "Test Task",
427+
});
442428

443-
expect(res.success).toBe(true);
444-
expect(h.client[clientMethod]).toHaveBeenCalled();
445-
},
446-
);
429+
expect(res.success).toBe(true);
430+
expect(h.client.pauseTask).toHaveBeenCalledWith("me", "task-1");
431+
expect(h.client.stopWorkspace).not.toHaveBeenCalled();
432+
});
433+
434+
it("resumeTask calls client.resumeTask", async () => {
435+
const h = createHarness();
436+
h.client.getTask.mockResolvedValue(task({ workspace_id: "ws-1" }));
437+
438+
const res = await h.request(TasksApi.resumeTask, {
439+
taskId: "task-1",
440+
taskName: "Test Task",
441+
});
442+
443+
expect(res.success).toBe(true);
444+
expect(h.client.resumeTask).toHaveBeenCalledWith("me", "task-1");
445+
expect(h.client.startWorkspace).not.toHaveBeenCalled();
446+
});
447+
448+
it("pauseTask falls back to stopWorkspace on 404", async () => {
449+
const h = createHarness();
450+
h.client.pauseTask.mockRejectedValue(createAxiosError(404, "Not found"));
451+
h.client.getTask.mockResolvedValue(task({ workspace_id: "ws-1" }));
452+
453+
const res = await h.request(TasksApi.pauseTask, {
454+
taskId: "task-1",
455+
taskName: "Test Task",
456+
});
457+
458+
expect(res.success).toBe(true);
459+
expect(h.client.stopWorkspace).toHaveBeenCalledWith("ws-1");
460+
});
461+
462+
it("resumeTask falls back to startWorkspace on 404", async () => {
463+
const h = createHarness();
464+
h.client.resumeTask.mockRejectedValue(
465+
createAxiosError(404, "Not found"),
466+
);
467+
h.client.getTask.mockResolvedValue(
468+
task({ workspace_id: "ws-1", template_version_id: "tv-1" }),
469+
);
470+
471+
const res = await h.request(TasksApi.resumeTask, {
472+
taskId: "task-1",
473+
taskName: "Test Task",
474+
});
447475

448-
it("pauseTask fails when no workspace", async () => {
476+
expect(res.success).toBe(true);
477+
expect(h.client.startWorkspace).toHaveBeenCalledWith("ws-1", "tv-1");
478+
});
479+
480+
it("caches legacy fallback after first 404", async () => {
449481
const h = createHarness();
482+
h.client.pauseTask.mockRejectedValue(createAxiosError(404, "Not found"));
483+
h.client.getTask.mockResolvedValue(task({ workspace_id: "ws-1" }));
484+
485+
await h.request(TasksApi.pauseTask, {
486+
taskId: "task-1",
487+
taskName: "Test Task",
488+
});
489+
h.client.pauseTask.mockClear();
490+
491+
await h.request(TasksApi.pauseTask, {
492+
taskId: "task-1",
493+
taskName: "Test Task",
494+
});
495+
496+
expect(h.client.pauseTask).not.toHaveBeenCalled();
497+
expect(h.client.stopWorkspace).toHaveBeenCalledTimes(2);
498+
});
499+
500+
it("propagates non-404 errors without fallback", async () => {
501+
const h = createHarness();
502+
h.client.pauseTask.mockRejectedValue(
503+
createAxiosError(500, "Internal server error"),
504+
);
505+
506+
const res = await h.request(TasksApi.pauseTask, {
507+
taskId: "task-1",
508+
taskName: "Test Task",
509+
});
510+
511+
expect(res.success).toBe(false);
512+
expect(h.client.stopWorkspace).not.toHaveBeenCalled();
513+
});
514+
515+
it("legacy pause fails when task has no workspace", async () => {
516+
const h = createHarness();
517+
h.client.pauseTask.mockRejectedValue(createAxiosError(404, "Not found"));
450518
h.client.getTask.mockResolvedValue(task({ workspace_id: null }));
451519

452520
const res = await h.request(TasksApi.pauseTask, {
@@ -719,7 +787,9 @@ describe("TasksPanelProvider", () => {
719787

720788
it("shows error notification for user action failures", async () => {
721789
const h = createHarness();
722-
h.client.getTask.mockRejectedValue(new Error("Workspace unavailable"));
790+
h.client.pauseTask.mockRejectedValue(
791+
new Error("Workspace unavailable"),
792+
);
723793

724794
const res = await h.request(TasksApi.pauseTask, {
725795
taskId: "task-1",

0 commit comments

Comments
 (0)