@@ -95,3 +95,121 @@ test('the read projects to a RECORD, not a bare scalar', async () => {
9595 assert . ok ( Array . isArray ( select ) , `select must be an array, got ${ typeof select } (${ select } )` ) ;
9696 }
9797} ) ;
98+
99+ /**
100+ * The `pending` substitution. `setPause` writes an intent and then immediately re-resolves
101+ * it in the same request — but a row deleted earlier in that request is still visible to
102+ * the read, so re-reading it resolves a resume back to "paused" and the API reports the
103+ * opposite of what happened. The fakes below return deliberately STALE rows to prove the
104+ * resolution trusts the just-written value instead of the table.
105+ */
106+
107+ test ( 'pending resume wins over a stale row that still reads as paused' , async ( ) => {
108+ // The delete already happened; the read just cannot see it yet.
109+ globalThis . databases = makeFakeDatabases ( { 'node-a' : { scope : 'node-a' , paused : true } } ) ;
110+
111+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-a' , paused : null } ) , {
112+ paused : false ,
113+ source : 'default' ,
114+ } ) ;
115+ } ) ;
116+
117+ test ( 'pending resume on a node still inherits a cluster pause' , async ( ) => {
118+ globalThis . databases = makeFakeDatabases ( {
119+ 'all' : { scope : 'all' , paused : true } ,
120+ 'node-a' : { scope : 'node-a' , paused : false } ,
121+ } ) ;
122+
123+ // Clearing the node override must fall back to the cluster row, not to "running".
124+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-a' , paused : null } ) , {
125+ paused : true ,
126+ source : 'cluster' ,
127+ } ) ;
128+ } ) ;
129+
130+ test ( 'pending pause wins over a stale row that has not appeared yet' , async ( ) => {
131+ globalThis . databases = makeFakeDatabases ( { } ) ;
132+
133+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-a' , paused : true } ) , {
134+ paused : true ,
135+ source : 'node' ,
136+ } ) ;
137+ } ) ;
138+
139+ test ( 'a pending cluster write leaves the node override authoritative' , async ( ) => {
140+ globalThis . databases = makeFakeDatabases ( { 'node-a' : { scope : 'node-a' , paused : false } } ) ;
141+
142+ // Cluster-wide pause requested, but this node is explicitly pinned to keep running.
143+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'all' , paused : true } ) , {
144+ paused : false ,
145+ source : 'node' ,
146+ } ) ;
147+ // A node with no override does take the pending cluster pause.
148+ assert . deepEqual ( await getDesiredPause ( 'node-b' , { scope : 'all' , paused : true } ) , {
149+ paused : true ,
150+ source : 'cluster' ,
151+ } ) ;
152+ } ) ;
153+
154+ test ( 'pending for an unrelated scope does not shadow either read' , async ( ) => {
155+ globalThis . databases = makeFakeDatabases ( { all : { scope : 'all' , paused : true } } ) ;
156+
157+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-z' , paused : false } ) , {
158+ paused : true ,
159+ source : 'cluster' ,
160+ } ) ;
161+ } ) ;
162+
163+ /**
164+ * `setDesiredPause` normalization. `setPause` threads `intent.paused` (what was written)
165+ * rather than its raw argument into the pending substitution, so these pin the property
166+ * that makes that safe: the returned `paused` is always a boolean or null, never undefined.
167+ */
168+
169+ test ( 'setDesiredPause normalizes a missing paused to false, and reports what it wrote' , async ( ) => {
170+ const writes = [ ] ;
171+ globalThis . databases = {
172+ render_service : {
173+ QueueControl : {
174+ put : ( scope , record ) => {
175+ writes . push ( { scope, ...record } ) ;
176+ return Promise . resolve ( ) ;
177+ } ,
178+ delete : ( scope ) => {
179+ writes . push ( { scope, deleted : true } ) ;
180+ return Promise . resolve ( ) ;
181+ } ,
182+ } ,
183+ } ,
184+ } ;
185+ const { setDesiredPause } = await import ( '../src/util/queueControl.js' ) ;
186+
187+ // An absent `paused` is written as false — so the value threaded into `pending` must be
188+ // that same normalized false, not undefined (which would resolve as "no opinion" and
189+ // contradict the row just written).
190+ const intent = await setDesiredPause ( 'node-a' , undefined , 'tester' ) ;
191+ assert . equal ( intent . paused , false ) ;
192+ assert . equal ( writes [ 0 ] . paused , false ) ;
193+
194+ // null deletes and reports null (inherit), which IS "no opinion" — correctly so.
195+ const cleared = await setDesiredPause ( 'node-a' , null , 'tester' ) ;
196+ assert . equal ( cleared . paused , null ) ;
197+ assert . equal ( cleared . inherited , true ) ;
198+ assert . equal ( writes [ 1 ] . deleted , true ) ;
199+ } ) ;
200+
201+ test ( 'a normalized false is an explicit override, distinct from "no opinion"' , async ( ) => {
202+ globalThis . databases = makeFakeDatabases ( { all : { scope : 'all' , paused : true } } ) ;
203+
204+ // paused:false must win over a cluster pause...
205+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-a' , paused : false } ) , {
206+ paused : false ,
207+ source : 'node' ,
208+ } ) ;
209+ // ...whereas undefined would silently fall through to it. This is exactly the confusion
210+ // threading `intent.paused` avoids.
211+ assert . deepEqual ( await getDesiredPause ( 'node-a' , { scope : 'node-a' , paused : undefined } ) , {
212+ paused : true ,
213+ source : 'cluster' ,
214+ } ) ;
215+ } ) ;
0 commit comments