11import assert from "node:assert/strict" ;
2- import { readFileSync } from "node:fs" ;
2+ import {
3+ chmodSync ,
4+ copyFileSync ,
5+ mkdirSync ,
6+ mkdtempSync ,
7+ readFileSync ,
8+ rmSync ,
9+ writeFileSync ,
10+ } from "node:fs" ;
11+ import { tmpdir } from "node:os" ;
12+ import { join } from "node:path" ;
13+ import { spawnSync } from "node:child_process" ;
314import { test } from "node:test" ;
415
516const iosAction = readFileSync (
@@ -25,6 +36,8 @@ function stepSlice(action, name, nextName) {
2536 return action . slice ( startIndex , endIndex ) ;
2637}
2738
39+ const darwinTest = process . platform === "darwin" ? test : test . skip ;
40+
2841test ( "iOS PR comment waits for public simulator list access" , ( ) => {
2942 const prebootIndex = iosAction . indexOf (
3043 "- name: Select and preboot simulator" ,
@@ -149,33 +162,20 @@ for (const [platform, action, startStep, waitStep] of [
149162 ) ;
150163 } ) ;
151164
152- test ( `${ platform } PR comment supervises recoverable daemon exits ` , ( ) => {
165+ test ( `${ platform } PR comment relies on the packaged CLI daemon supervisor ` , ( ) => {
153166 const startStepBody = stepSlice (
154167 action ,
155168 "Install tools, start SimDeck and tunnel" ,
156169 "Resolve PR head" ,
157170 ) ;
158171
159- assert . match (
172+ assert . doesNotMatch (
160173 startStepBody ,
161174 / s i m d e c k - d a e m o n - s u p e r v i s o r \. s h / ,
162- "action should run SimDeck through a local supervisor" ,
163- ) ;
164- assert . match (
165- startStepBody ,
166- / " \$ \{ s t a t u s \} " - e q 7 5 / ,
167- "supervisor should restart recoverable SimDeck exits" ,
168- ) ;
169- assert . match (
170- startStepBody ,
171- / " \$ \{ s t a t u s \} " - g e 1 2 8 / ,
172- "supervisor should restart signal-terminated daemon children" ,
173- ) ;
174- assert . match (
175- startStepBody ,
176- / s i m d e c k - c h i l d \. p i d / ,
177- "supervisor should expose the active child pid for cleanup diagnostics" ,
175+ "action should not carry a second workflow-local daemon supervisor" ,
178176 ) ;
177+ assert . match ( startStepBody , / s i m d e c k d a e m o n r u n / ) ;
178+ assert . match ( startStepBody , / e c h o " \$ ! " > s i m d e c k \. p i d / ) ;
179179 } ) ;
180180
181181 test ( `${ platform } PR comment keepalive tolerates transient daemon restarts` , ( ) => {
@@ -207,3 +207,97 @@ for (const [platform, action, startStep, waitStep] of [
207207 ) ;
208208 } ) ;
209209}
210+
211+ darwinTest (
212+ "npm CLI wrapper restarts daemon run after recoverable native exit" ,
213+ ( ) => {
214+ const root = mkdtempSync ( join ( tmpdir ( ) , "simdeck-wrapper-test-" ) ) ;
215+ try {
216+ mkdirSync ( join ( root , "bin" ) , { recursive : true } ) ;
217+ mkdirSync ( join ( root , "build" ) , { recursive : true } ) ;
218+ const wrapperPath = join ( root , "bin" , "simdeck.mjs" ) ;
219+ const nativePath = join ( root , "build" , "simdeck-bin" ) ;
220+ const logPath = join ( root , "native.log" ) ;
221+ const countPath = join ( root , "count" ) ;
222+
223+ copyFileSync ( new URL ( "../bin/simdeck.mjs" , import . meta. url ) , wrapperPath ) ;
224+ chmodSync ( wrapperPath , 0o755 ) ;
225+ writeFileSync (
226+ nativePath ,
227+ `#!/usr/bin/env bash
228+ set -euo pipefail
229+ count="$(cat "${ countPath } " 2>/dev/null || echo 0)"
230+ count="$((count + 1))"
231+ echo "$count" > "${ countPath } "
232+ echo "$$:\${SIMDECK_DAEMON_METADATA_PID:-}:\$*" >> "${ logPath } "
233+ if [[ "$count" == "1" ]]; then
234+ exit 75
235+ fi
236+ exit 0
237+ ` ,
238+ ) ;
239+ chmodSync ( nativePath , 0o755 ) ;
240+
241+ const result = spawnSync (
242+ process . execPath ,
243+ [ wrapperPath , "daemon" , "run" , "--port" , "4310" ] ,
244+ {
245+ encoding : "utf8" ,
246+ } ,
247+ ) ;
248+
249+ assert . equal ( result . status , 0 , result . stderr ) ;
250+ const logLines = readFileSync ( logPath , "utf8" ) . trim ( ) . split ( "\n" ) ;
251+ assert . equal ( logLines . length , 2 , "daemon run should be retried once" ) ;
252+
253+ const entries = logLines . map ( ( line ) => {
254+ const [ pid , metadataPid , args ] = line . split ( ":" ) ;
255+ return { pid, metadataPid, args } ;
256+ } ) ;
257+ assert . notEqual ( entries [ 0 ] . pid , entries [ 1 ] . pid ) ;
258+ assert . match ( entries [ 0 ] . metadataPid , / ^ \d + $ / ) ;
259+ assert . equal ( entries [ 0 ] . metadataPid , entries [ 1 ] . metadataPid ) ;
260+ assert . notEqual ( entries [ 0 ] . pid , entries [ 0 ] . metadataPid ) ;
261+ assert . equal ( entries [ 0 ] . args , "daemon run --port 4310" ) ;
262+ } finally {
263+ rmSync ( root , { recursive : true , force : true } ) ;
264+ }
265+ } ,
266+ ) ;
267+
268+ darwinTest (
269+ "npm CLI wrapper does not restart non-daemon commands on exit 75" ,
270+ ( ) => {
271+ const root = mkdtempSync ( join ( tmpdir ( ) , "simdeck-wrapper-test-" ) ) ;
272+ try {
273+ mkdirSync ( join ( root , "bin" ) , { recursive : true } ) ;
274+ mkdirSync ( join ( root , "build" ) , { recursive : true } ) ;
275+ const wrapperPath = join ( root , "bin" , "simdeck.mjs" ) ;
276+ const nativePath = join ( root , "build" , "simdeck-bin" ) ;
277+ const logPath = join ( root , "native.log" ) ;
278+
279+ copyFileSync ( new URL ( "../bin/simdeck.mjs" , import . meta. url ) , wrapperPath ) ;
280+ chmodSync ( wrapperPath , 0o755 ) ;
281+ writeFileSync (
282+ nativePath ,
283+ `#!/usr/bin/env bash
284+ set -euo pipefail
285+ echo "$$:\${SIMDECK_DAEMON_METADATA_PID:-}:\$*" >> "${ logPath } "
286+ exit 75
287+ ` ,
288+ ) ;
289+ chmodSync ( nativePath , 0o755 ) ;
290+
291+ const result = spawnSync ( process . execPath , [ wrapperPath , "list" ] , {
292+ encoding : "utf8" ,
293+ } ) ;
294+
295+ assert . equal ( result . status , 75 ) ;
296+ const logLines = readFileSync ( logPath , "utf8" ) . trim ( ) . split ( "\n" ) ;
297+ assert . equal ( logLines . length , 1 ) ;
298+ assert . equal ( logLines [ 0 ] . split ( ":" ) [ 1 ] , "" ) ;
299+ } finally {
300+ rmSync ( root , { recursive : true , force : true } ) ;
301+ }
302+ } ,
303+ ) ;
0 commit comments