@@ -91,6 +91,21 @@ describe('hook-status', () => {
9191 // used by the Windows branch is exercised on every platform's CI run.
9292 const body = ( ) => buildStatusLinePython ( STATUS_DIR ) ;
9393
94+ // Feeds the script to python3 on stdin. PYTHONIOENCODING is required:
95+ // Node writes `input` as UTF-8, but on Windows sys.stdin defaults to the
96+ // locale codepage, so a non-ascii path comes back mojibake'd (\u00fc ->
97+ // \u00c3\u00bc). Production is unaffected — the body is written to a .py file,
98+ // and Python reads source files as UTF-8 regardless of locale.
99+ const runPython = ( code : string , input : string ) => {
100+ const { spawnSync } = require ( 'child_process' ) as typeof import ( 'child_process' ) ;
101+ const opts = { input, env : { ...process . env , PYTHONIOENCODING : 'utf-8' } } ;
102+ return spawnSync ( 'python3' , [ '-c' , code ] , opts ) ;
103+ } ;
104+ const hasPython = ( ) => {
105+ const { spawnSync } = require ( 'child_process' ) as typeof import ( 'child_process' ) ;
106+ return ! spawnSync ( 'python3' , [ '-c' , 'pass' ] ) . error ;
107+ } ;
108+
94109 it ( 'extracts cost, context_window, session_id and session_name' , ( ) => {
95110 const script = body ( ) ;
96111 for ( const field of [ 'cost' , 'context_window' , 'session_id' , 'session_name' ] ) {
@@ -121,13 +136,12 @@ describe('hook-status', () => {
121136 const literal = script . split ( '\n' ) . find ( ( l ) => l . startsWith ( 'status_dir=' ) ) ! ;
122137 expect ( literal ) . toBe ( `status_dir=${ JSON . stringify ( dir ) } ` ) ;
123138
124- const { spawnSync } = require ( 'child_process' ) as typeof import ( 'child_process' ) ;
125- if ( spawnSync ( 'python3' , [ '-c' , 'pass' ] ) . error ) return ; // no python3 here
139+ if ( ! hasPython ( ) ) return ; // no python3 here
126140
127- const result = spawnSync ( 'python3' , [
128- '-c' ,
141+ const result = runPython (
129142 `import sys,json;ns={};exec(sys.stdin.read(),ns);print(json.dumps(ns["status_dir"]))` ,
130- ] , { input : literal } ) ;
143+ literal ,
144+ ) ;
131145 expect ( result . status ) . toBe ( 0 ) ;
132146 expect ( JSON . parse ( result . stdout . toString ( ) ) ) . toBe ( dir ) ;
133147 } ) ;
@@ -144,14 +158,9 @@ describe('hook-status', () => {
144158 // A syntax error here is silent in production: it kills cost, context,
145159 // sessionid and name at once, with the traceback going only to
146160 // statusline.log. Nothing else in this suite would catch it.
147- const { spawnSync } = require ( 'child_process' ) as typeof import ( 'child_process' ) ;
148- const probe = spawnSync ( 'python3' , [ '-c' , 'pass' ] ) ;
149- if ( probe . error ) return ; // no python3 on this runner
161+ if ( ! hasPython ( ) ) return ; // no python3 on this runner
150162
151- const result = spawnSync ( 'python3' , [
152- '-c' ,
153- 'import sys;compile(sys.stdin.read(),"statusline","exec")' ,
154- ] , { input : body ( ) } ) ;
163+ const result = runPython ( 'import sys;compile(sys.stdin.read(),"statusline","exec")' , body ( ) ) ;
155164
156165 expect ( result . stderr . toString ( ) ) . toBe ( '' ) ;
157166 expect ( result . status ) . toBe ( 0 ) ;
0 commit comments