@@ -4,6 +4,7 @@ import os from "node:os";
44import path from "node:path" ;
55import { expectDefined } from "@openclaw/normalization-core" ;
66import { afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
7+ import { decodeWindowsLauncherScript } from "../daemon/launcher-encoding.js" ;
78import { captureFullEnv } from "../test-utils/env.js" ;
89import { getWindowsCmdExePath } from "./windows-install-roots.js" ;
910
@@ -15,6 +16,9 @@ const resolveTaskScriptPathMock = vi.hoisted(() =>
1516 return path . join ( home , ".openclaw" , "gateway.cmd" ) ;
1617 } ) ,
1718) ;
19+ // Pin code page detection so hosts with CJK home paths cannot leak the real
20+ // PowerShell probe into script-encoding assertions.
21+ const resolveWindowsSystemEncodingMock = vi . hoisted ( ( ) => vi . fn ( ( ) : string | null => null ) ) ;
1822
1923vi . mock ( "node:child_process" , async ( ) => {
2024 const { mockNodeBuiltinModule } = await import ( "openclaw/plugin-sdk/test-node-mocks" ) ;
@@ -32,6 +36,14 @@ vi.mock("../daemon/schtasks.js", () => ({
3236 resolveTaskScriptPath : ( env : Record < string , string | undefined > ) =>
3337 resolveTaskScriptPathMock ( env ) ,
3438} ) ) ;
39+ vi . mock ( "./windows-encoding.js" , async ( ) => {
40+ const actual =
41+ await vi . importActual < typeof import ( "./windows-encoding.js" ) > ( "./windows-encoding.js" ) ;
42+ return {
43+ ...actual ,
44+ resolveWindowsSystemEncoding : ( ) => resolveWindowsSystemEncodingMock ( ) ,
45+ } ;
46+ } ) ;
3547
3648type WindowsTaskRestartModule = typeof import ( "./windows-task-restart.js" ) ;
3749
@@ -90,6 +102,8 @@ describe("relaunchGatewayScheduledTask", () => {
90102 const home = env . USERPROFILE || env . HOME || os . homedir ( ) ;
91103 return path . join ( home , ".openclaw" , "gateway.cmd" ) ;
92104 } ) ;
105+ resolveWindowsSystemEncodingMock . mockReset ( ) ;
106+ resolveWindowsSystemEncodingMock . mockReturnValue ( null ) ;
93107 } ) ;
94108
95109 it ( "writes a detached schtasks relaunch helper" , ( ) => {
@@ -124,6 +138,8 @@ describe("relaunchGatewayScheduledTask", () => {
124138 }
125139 expect ( fs . statSync ( scriptPath ) . isFile ( ) ) . toBe ( true ) ;
126140 const script = fs . readFileSync ( scriptPath , "utf8" ) ;
141+ // ASCII helper scripts stay marker-free UTF-8 bytes.
142+ expect ( script . startsWith ( "@echo off\r\n" ) ) . toBe ( true ) ;
127143 expect ( script ) . toContain ( "timeout /t 1 /nobreak >nul" ) ;
128144 expect ( script ) . toContain ( "gateway-restart.log" ) ;
129145 expect ( script ) . toContain (
@@ -248,4 +264,57 @@ describe("relaunchGatewayScheduledTask", () => {
248264 expect ( script ) . toContain ( `start "" /min ${ getWindowsCmdExePath ( ) } /d /c` ) ;
249265 expect ( script ) . toContain ( taskScriptPath ) ;
250266 } ) ;
267+
268+ // Pin the host home/state paths embedded in the script to ASCII so the only
269+ // code-page-sensitive content in the gbk tests is the task name under test;
270+ // otherwise a non-GBK Windows username (Hangul/Thai/...) fails the encode.
271+ const asciiPathEnv = {
272+ HOME : "C:\\ocw-test" ,
273+ USERPROFILE : "C:\\ocw-test" ,
274+ OPENCLAW_STATE_DIR : "C:\\ocw-test\\state" ,
275+ } ;
276+
277+ it ( "writes marked code-page bytes for CJK task names that decode back exactly" , ( ) => {
278+ resolveWindowsSystemEncodingMock . mockReturnValue ( "gbk" ) ;
279+ spawnMock . mockImplementation ( ( _file : string , args : string [ ] ) => {
280+ createdScriptPaths . add ( decodeCmdPathArg ( expectDefined ( args [ 3 ] , "args[3] test invariant" ) ) ) ;
281+ return { unref : vi . fn ( ) } ;
282+ } ) ;
283+
284+ const result = relaunchGatewayScheduledTask ( {
285+ ...asciiPathEnv ,
286+ OPENCLAW_WINDOWS_TASK_NAME : "OpenClaw Gateway (隆)" ,
287+ } ) ;
288+
289+ expect ( result . ok ) . toBe ( true ) ;
290+ const scriptPath = expectDefined (
291+ [ ...createdScriptPaths ] [ 0 ] ,
292+ "[...createdScriptPaths][0] test invariant" ,
293+ ) ;
294+ const raw = fs . readFileSync ( scriptPath ) ;
295+ expect ( raw . toString ( "latin1" ) . startsWith ( "@rem openclaw-launcher-encoding=gbk\r\n" ) ) . toBe ( true ) ;
296+ // The old raw-UTF-8 writer would have kept the task name readable here.
297+ expect ( raw . toString ( "utf8" ) ) . not . toContain ( "隆" ) ;
298+ const script = decodeWindowsLauncherScript ( { buffer : raw } ) ;
299+ expect ( script . startsWith ( "@echo off\r\n" ) ) . toBe ( true ) ;
300+ expect ( script ) . toContain ( 'schtasks /Run /TN "OpenClaw Gateway (隆)" >>' ) ;
301+ expect ( script ) . toContain ( 'del "%~f0" >nul 2>&1' ) ;
302+ } ) ;
303+
304+ it ( "returns failed instead of writing an unrepresentable helper script" , ( ) => {
305+ resolveWindowsSystemEncodingMock . mockReturnValue ( "gbk" ) ;
306+ spawnMock . mockImplementation ( ( ) => {
307+ throw new Error ( "spawn should not be reached" ) ;
308+ } ) ;
309+
310+ const result = relaunchGatewayScheduledTask ( {
311+ ...asciiPathEnv ,
312+ OPENCLAW_WINDOWS_TASK_NAME : "🚀" ,
313+ } ) ;
314+
315+ expect ( result . ok ) . toBe ( false ) ;
316+ expect ( result . method ) . toBe ( "schtasks" ) ;
317+ expect ( result . detail ) . toMatch ( / c a n n o t b e r e p r e s e n t e d / ) ;
318+ expect ( spawnMock ) . not . toHaveBeenCalled ( ) ;
319+ } ) ;
251320} ) ;
0 commit comments