1+ /**
2+ * @license
3+ * Copyright 2025 Google LLC
4+ * SPDX-License-Identifier: Apache-2.0
5+ */
6+
17import { describe , it , expect , beforeEach , afterEach } from 'vitest' ;
28import * as fs from 'node:fs' ;
39import * as path from 'node:path' ;
410import * as os from 'node:os' ;
5- import { rollbackStandaloneUpdate } from './standalone-update.js' ;
11+ import {
12+ rollbackStandaloneUpdate ,
13+ ensureBinWrapper ,
14+ ensurePathInShellRc ,
15+ } from './standalone-update.js' ;
616
717describe ( 'standalone-update' , ( ) => {
818 let tempDir : string ;
@@ -54,7 +64,6 @@ describe('standalone-update', () => {
5464 fs . mkdirSync ( standaloneDir ) ;
5565 fs . mkdirSync ( oldDir ) ;
5666
57- // Current version
5867 fs . writeFileSync (
5968 path . join ( standaloneDir , 'manifest.json' ) ,
6069 JSON . stringify ( {
@@ -65,7 +74,6 @@ describe('standalone-update', () => {
6574 ) ;
6675 fs . writeFileSync ( path . join ( standaloneDir , 'marker.txt' ) , 'new' ) ;
6776
68- // Old version
6977 fs . writeFileSync (
7078 path . join ( oldDir , 'manifest.json' ) ,
7179 JSON . stringify ( {
@@ -79,20 +87,17 @@ describe('standalone-update', () => {
7987 const result = rollbackStandaloneUpdate ( standaloneDir ) ;
8088 expect ( result ) . toBe ( true ) ;
8189
82- // Verify the swap happened
8390 const manifest = JSON . parse (
8491 fs . readFileSync ( path . join ( standaloneDir , 'manifest.json' ) , 'utf-8' ) ,
8592 ) ;
8693 expect ( manifest . version ) . toBe ( '0.16.2' ) ;
8794 expect (
8895 fs . readFileSync ( path . join ( standaloneDir , 'marker.txt' ) , 'utf-8' ) ,
8996 ) . toBe ( 'old' ) ;
90-
91- // .old should no longer exist
9297 expect ( fs . existsSync ( oldDir ) ) . toBe ( false ) ;
9398 } ) ;
9499
95- it ( 'returns false if .old has invalid manifest content ' , ( ) => {
100+ it ( 'succeeds even with minimal manifest in .old ' , ( ) => {
96101 const standaloneDir = path . join ( tempDir , 'qwen-code' ) ;
97102 const oldDir = `${ standaloneDir } .old` ;
98103 fs . mkdirSync ( standaloneDir ) ;
@@ -102,12 +107,143 @@ describe('standalone-update', () => {
102107 path . join ( standaloneDir , 'manifest.json' ) ,
103108 JSON . stringify ( { name : '@qwen-code/qwen-code' , version : '0.17.0' } ) ,
104109 ) ;
105- // Old dir has manifest — rollback should succeed even with minimal manifest
106110 fs . writeFileSync ( path . join ( oldDir , 'manifest.json' ) , '{}' ) ;
107111
108112 const result = rollbackStandaloneUpdate ( standaloneDir ) ;
109- // It should succeed because manifest.json EXISTS (content validation is not done in rollback)
110113 expect ( result ) . toBe ( true ) ;
111114 } ) ;
112115 } ) ;
116+
117+ describe ( 'ensureBinWrapper' , ( ) => {
118+ it ( 'creates a Unix shell wrapper script' , ( ) => {
119+ const libDir = path . join ( tempDir , '.local' , 'lib' ) ;
120+ const standaloneDir = path . join ( libDir , 'qwen-code' ) ;
121+ fs . mkdirSync ( standaloneDir , { recursive : true } ) ;
122+
123+ // Isolate HOME so ensurePathInShellRc doesn't touch real shell rc
124+ const origHome = process . env [ 'HOME' ] ;
125+ const origShell = process . env [ 'SHELL' ] ;
126+ process . env [ 'HOME' ] = tempDir ;
127+ process . env [ 'SHELL' ] = '/bin/zsh' ;
128+ try {
129+ ensureBinWrapper ( standaloneDir , 'darwin-arm64' ) ;
130+ } finally {
131+ process . env [ 'HOME' ] = origHome ;
132+ process . env [ 'SHELL' ] = origShell ;
133+ }
134+
135+ const wrapperPath = path . join ( tempDir , '.local' , 'bin' , 'qwen' ) ;
136+ expect ( fs . existsSync ( wrapperPath ) ) . toBe ( true ) ;
137+ const content = fs . readFileSync ( wrapperPath , 'utf-8' ) ;
138+ expect ( content ) . toContain ( '#!/bin/sh' ) ;
139+ expect ( content ) . toContain ( standaloneDir ) ;
140+ const mode = fs . statSync ( wrapperPath ) . mode ;
141+ expect ( mode & 0o111 ) . toBeGreaterThan ( 0 ) ;
142+ } ) ;
143+
144+ it ( 'creates a Windows cmd wrapper' , ( ) => {
145+ const libDir = path . join ( tempDir , '.local' , 'lib' ) ;
146+ const standaloneDir = path . join ( libDir , 'qwen-code' ) ;
147+ fs . mkdirSync ( standaloneDir , { recursive : true } ) ;
148+
149+ ensureBinWrapper ( standaloneDir , 'win-x64' ) ;
150+
151+ const wrapperPath = path . join ( tempDir , '.local' , 'bin' , 'qwen.cmd' ) ;
152+ expect ( fs . existsSync ( wrapperPath ) ) . toBe ( true ) ;
153+ const content = fs . readFileSync ( wrapperPath , 'utf-8' ) ;
154+ expect ( content ) . toContain ( '@echo off' ) ;
155+ } ) ;
156+
157+ it ( 'does not overwrite existing wrapper' , ( ) => {
158+ const libDir = path . join ( tempDir , '.local' , 'lib' ) ;
159+ const standaloneDir = path . join ( libDir , 'qwen-code' ) ;
160+ const binDir = path . join ( tempDir , '.local' , 'bin' ) ;
161+ fs . mkdirSync ( standaloneDir , { recursive : true } ) ;
162+ fs . mkdirSync ( binDir , { recursive : true } ) ;
163+
164+ const origHome = process . env [ 'HOME' ] ;
165+ const origShell = process . env [ 'SHELL' ] ;
166+ process . env [ 'HOME' ] = tempDir ;
167+ process . env [ 'SHELL' ] = '/bin/zsh' ;
168+
169+ const wrapperPath = path . join ( binDir , 'qwen' ) ;
170+ fs . writeFileSync ( wrapperPath , 'existing-content' , { mode : 0o755 } ) ;
171+
172+ try {
173+ ensureBinWrapper ( standaloneDir , 'linux-x64' ) ;
174+ expect ( fs . readFileSync ( wrapperPath , 'utf-8' ) ) . toBe ( 'existing-content' ) ;
175+ } finally {
176+ process . env [ 'HOME' ] = origHome ;
177+ process . env [ 'SHELL' ] = origShell ;
178+ }
179+ } ) ;
180+ } ) ;
181+
182+ describe ( 'ensurePathInShellRc' , ( ) => {
183+ it ( 'appends PATH export to zshrc when SHELL is zsh' , ( ) => {
184+ const binDir = path . join ( tempDir , 'bin' ) ;
185+ const zshrc = path . join ( tempDir , '.zshrc' ) ;
186+ fs . writeFileSync ( zshrc , '# existing config\n' ) ;
187+
188+ const origShell = process . env [ 'SHELL' ] ;
189+ const origHome = process . env [ 'HOME' ] ;
190+ process . env [ 'SHELL' ] = '/bin/zsh' ;
191+ process . env [ 'HOME' ] = tempDir ;
192+
193+ try {
194+ ensurePathInShellRc ( binDir ) ;
195+ const content = fs . readFileSync ( zshrc , 'utf-8' ) ;
196+ expect ( content ) . toContain ( '# Added by Qwen Code standalone installer' ) ;
197+ expect ( content ) . toContain ( `export PATH="${ binDir } :$PATH"` ) ;
198+ } finally {
199+ process . env [ 'SHELL' ] = origShell ;
200+ process . env [ 'HOME' ] = origHome ;
201+ }
202+ } ) ;
203+
204+ it ( 'skips if marker already in rc file' , ( ) => {
205+ const binDir = path . join ( tempDir , 'bin' ) ;
206+ const zshrc = path . join ( tempDir , '.zshrc' ) ;
207+ fs . writeFileSync (
208+ zshrc ,
209+ `# Added by Qwen Code standalone installer\nexport PATH="${ binDir } :$PATH"\n` ,
210+ ) ;
211+
212+ const origShell = process . env [ 'SHELL' ] ;
213+ const origHome = process . env [ 'HOME' ] ;
214+ process . env [ 'SHELL' ] = '/bin/zsh' ;
215+ process . env [ 'HOME' ] = tempDir ;
216+
217+ try {
218+ ensurePathInShellRc ( binDir ) ;
219+ const content = fs . readFileSync ( zshrc , 'utf-8' ) ;
220+ const matches = content . match (
221+ / # A d d e d b y Q w e n C o d e s t a n d a l o n e i n s t a l l e r / g,
222+ ) ;
223+ expect ( matches ) . toHaveLength ( 1 ) ;
224+ } finally {
225+ process . env [ 'SHELL' ] = origShell ;
226+ process . env [ 'HOME' ] = origHome ;
227+ }
228+ } ) ;
229+
230+ it ( 'does nothing for unknown shells' , ( ) => {
231+ const binDir = path . join ( tempDir , 'bin' ) ;
232+ const origShell = process . env [ 'SHELL' ] ;
233+ const origHome = process . env [ 'HOME' ] ;
234+ process . env [ 'SHELL' ] = '/bin/csh' ;
235+ process . env [ 'HOME' ] = tempDir ;
236+
237+ try {
238+ ensurePathInShellRc ( binDir ) ;
239+ // No rc file should be created
240+ expect (
241+ fs . readdirSync ( tempDir ) . filter ( ( f ) => f . startsWith ( '.' ) ) ,
242+ ) . toHaveLength ( 0 ) ;
243+ } finally {
244+ process . env [ 'SHELL' ] = origShell ;
245+ process . env [ 'HOME' ] = origHome ;
246+ }
247+ } ) ;
248+ } ) ;
113249} ) ;
0 commit comments