@@ -14,23 +14,32 @@ import { workspace as createWorkspace } from "@repo/mocks";
1414
1515import type { WorkspaceBuild } from "coder/site/src/api/typesGenerated" ;
1616
17- function setup ( status : "running" | "stopped" | "failed" = "running" ) {
17+ const parameters = [ { name : "region" , value : "east" } ] ;
18+ const expectedStart = {
19+ transition : "start" as const ,
20+ template_version_id : "target-version" ,
21+ rich_parameter_values : parameters ,
22+ } ;
23+
24+ function setup (
25+ status : "running" | "stopped" = "running" ,
26+ jobStatus : WorkspaceBuild [ "job" ] [ "status" ] = "pending" ,
27+ ) {
1828 const client = new Api ( ) ;
1929 const http = axios . create ( ) ;
20- vi . spyOn ( client , "getAxiosInstance" ) . mockReturnValue ( http ) ;
2130 const source = createWorkspace ( {
2231 template_active_version_id : "target-version" ,
2332 latest_build : { status, build_number : 1 } ,
2433 } ) ;
2534 const acceptedBuild : WorkspaceBuild = {
2635 ...source . latest_build ,
2736 id : "accepted-build" ,
28- workspace_id : source . id ,
2937 build_number : 2 ,
3038 transition : status === "running" ? "stop" : "start" ,
3139 template_version_id : "target-version" ,
32- job : { ...source . latest_build . job , status : "pending" } ,
40+ job : { ...source . latest_build . job , status : jobStatus } ,
3341 } ;
42+ vi . spyOn ( client , "getAxiosInstance" ) . mockReturnValue ( http ) ;
3443 const getWorkspace = vi
3544 . spyOn ( client , "getWorkspace" )
3645 . mockResolvedValue ( source ) ;
@@ -40,6 +49,10 @@ function setup(status: "running" | "stopped" | "failed" = "running") {
4049 return { client, source, acceptedBuild, getWorkspace, post } ;
4150}
4251
52+ function buildInfo ( version : string ) : Awaited < ReturnType < Api [ "getBuildInfo" ] > > {
53+ return { version } as Awaited < ReturnType < Api [ "getBuildInfo" ] > > ;
54+ }
55+
4356function responseError ( status : number ) : AxiosError {
4457 return new AxiosError ( "request failed" , undefined , undefined , undefined , {
4558 status,
@@ -68,9 +81,7 @@ describe("server capability and preflight", () => {
6881 "does not require template permission for a stopped workspace on %s" ,
6982 async ( version ) => {
7083 const { client, source } = setup ( "stopped" ) ;
71- vi . spyOn ( client , "getBuildInfo" ) . mockResolvedValue ( { version } as Awaited <
72- ReturnType < Api [ "getBuildInfo" ] >
73- > ) ;
84+ vi . spyOn ( client , "getBuildInfo" ) . mockResolvedValue ( buildInfo ( version ) ) ;
7485 const auth = vi . spyOn ( client , "checkAuthorization" ) ;
7586
7687 await preflightWorkspaceUpdate ( client , source ) ;
@@ -83,9 +94,7 @@ describe("server capability and preflight", () => {
8394 "refuses denied or unverifiable permission: %s" ,
8495 async ( permission ) => {
8596 const { client, source, post } = setup ( ) ;
86- vi . spyOn ( client , "getBuildInfo" ) . mockResolvedValue ( {
87- version : "2.36.0" ,
88- } as Awaited < ReturnType < Api [ "getBuildInfo" ] > > ) ;
97+ vi . spyOn ( client , "getBuildInfo" ) . mockResolvedValue ( buildInfo ( "2.36.0" ) ) ;
8998 const auth = vi . spyOn ( client , "checkAuthorization" ) ;
9099 if ( permission === "error" ) auth . mockRejectedValue ( new Error ( "secret" ) ) ;
91100 else auth . mockResolvedValue ( { templateUpdate : false } ) ;
@@ -99,57 +108,34 @@ describe("server capability and preflight", () => {
99108} ) ;
100109
101110describe ( "pinned update acceptance" , ( ) => {
102- it . each ( [ "pending" , "succeeded" ] as const ) (
103- "returns an accepted direct start without waiting for completion: %s" ,
104- async ( jobStatus ) => {
105- const { client, source, acceptedBuild, post } = setup ( "stopped" ) ;
106- const accepted = {
107- ...acceptedBuild ,
108- job : { ...acceptedBuild . job , status : jobStatus } ,
109- } ;
110- post . mockResolvedValue ( { data : accepted } ) ;
111+ it . each ( [
112+ [ "stopped" , "pending" , { ...expectedStart , reason : "vscode_connection" } ] ,
113+ [ "stopped" , "succeeded" , { ...expectedStart , reason : "vscode_connection" } ] ,
114+ [
115+ "running" ,
116+ "pending" ,
117+ {
118+ transition : "stop" ,
119+ reason : "vscode_connection" ,
120+ on_success : expectedStart ,
121+ } ,
122+ ] ,
123+ ] as const ) (
124+ "accepts %s workspace update with %s build job" ,
125+ async ( status , jobStatus , request ) => {
126+ const { client, source, acceptedBuild, post } = setup ( status , jobStatus ) ;
111127
112128 await expect (
113- updateWorkspaceWithPinnedBuild ( client , source , [
114- { name : "region" , value : "east" } ,
115- ] ) ,
116- ) . resolves . toEqual ( { ...source , latest_build : accepted } ) ;
129+ updateWorkspaceWithPinnedBuild ( client , source , parameters ) ,
130+ ) . resolves . toEqual ( { ...source , latest_build : acceptedBuild } ) ;
117131 expect ( post ) . toHaveBeenCalledExactlyOnceWith (
118132 `/api/v2/workspaces/${ source . id } /builds` ,
119- {
120- transition : "start" ,
121- template_version_id : "target-version" ,
122- rich_parameter_values : [ { name : "region" , value : "east" } ] ,
123- reason : "vscode_connection" ,
124- } ,
133+ request ,
125134 { signal : undefined , sensitive : true } ,
126135 ) ;
127136 } ,
128137 ) ;
129138
130- it ( "queues a pinned restart for a running workspace and returns its accepted stop build" , async ( ) => {
131- const { client, source, acceptedBuild, post } = setup ( ) ;
132-
133- await expect (
134- updateWorkspaceWithPinnedBuild ( client , source , [
135- { name : "region" , value : "east" } ,
136- ] ) ,
137- ) . resolves . toEqual ( { ...source , latest_build : acceptedBuild } ) ;
138- expect ( post ) . toHaveBeenCalledExactlyOnceWith (
139- `/api/v2/workspaces/${ source . id } /builds` ,
140- {
141- transition : "stop" ,
142- reason : "vscode_connection" ,
143- on_success : {
144- transition : "start" ,
145- template_version_id : "target-version" ,
146- rich_parameter_values : [ { name : "region" , value : "east" } ] ,
147- } ,
148- } ,
149- { signal : undefined , sensitive : true } ,
150- ) ;
151- } ) ;
152-
153139 it . each ( [ "build" , "version" ] as const ) (
154140 "rejects source %s drift before queueing" ,
155141 async ( drift ) => {
@@ -172,17 +158,12 @@ describe("pinned update acceptance", () => {
172158 const { client, source, post } = setup ( ) ;
173159 post . mockRejectedValue ( new Error ( "secret server detail" ) ) ;
174160
175- const error = await updateWorkspaceWithPinnedBuild (
176- client ,
177- source ,
178- [ ] ,
179- ) . catch ( ( error : unknown ) => error ) ;
180-
181- expect ( error ) . toBeInstanceOf ( WorkspaceUpdateFailedError ) ;
182- expect ( error ) . toHaveProperty (
183- "message" ,
184- "Workspace update failed or could not be queued safely. Check the workspace in the dashboard before retrying." ,
185- ) ;
161+ const result = updateWorkspaceWithPinnedBuild ( client , source , [ ] ) ;
162+ await expect ( result ) . rejects . toBeInstanceOf ( WorkspaceUpdateFailedError ) ;
163+ await expect ( result ) . rejects . toMatchObject ( {
164+ message :
165+ "Workspace update failed or could not be queued safely. Check the workspace in the dashboard before retrying." ,
166+ } ) ;
186167 expect ( post ) . toHaveBeenCalledOnce ( ) ;
187168 } ) ;
188169
@@ -196,27 +177,24 @@ describe("pinned update acceptance", () => {
196177 expect ( post ) . toHaveBeenCalledOnce ( ) ;
197178 } ) ;
198179
199- it ( "aborts after an accepted request" , async ( ) => {
180+ it . each ( [
181+ [ "after an accepted request" , true , 1 ] ,
182+ [ "before sending a request" , false , 0 ] ,
183+ ] as const ) ( "aborts %s" , async ( _when , abortAfterRequest , requestCount ) => {
200184 const { client, source, post } = setup ( ) ;
201185 const controller = new AbortController ( ) ;
202- post . mockImplementation ( ( ) => {
186+ if ( abortAfterRequest ) {
187+ post . mockImplementation ( ( ) => {
188+ controller . abort ( ) ;
189+ return Promise . resolve ( { data : source . latest_build } ) ;
190+ } ) ;
191+ } else {
203192 controller . abort ( ) ;
204- return Promise . resolve ( { data : source . latest_build } ) ;
205- } ) ;
206-
207- await expect (
208- updateWorkspaceWithPinnedBuild ( client , source , [ ] , controller . signal ) ,
209- ) . rejects . toMatchObject ( { name : "AbortError" } ) ;
210- expect ( post ) . toHaveBeenCalledOnce ( ) ;
211- } ) ;
212- it ( "does not send a request when aborted" , async ( ) => {
213- const { client, source, post } = setup ( ) ;
214- const controller = new AbortController ( ) ;
215- controller . abort ( ) ;
193+ }
216194
217195 await expect (
218196 updateWorkspaceWithPinnedBuild ( client , source , [ ] , controller . signal ) ,
219197 ) . rejects . toMatchObject ( { name : "AbortError" } ) ;
220- expect ( post ) . not . toHaveBeenCalled ( ) ;
198+ expect ( post ) . toHaveBeenCalledTimes ( requestCount ) ;
221199 } ) ;
222200} ) ;
0 commit comments