@@ -4,78 +4,114 @@ import { describe, expect, it } from "vitest";
44import { withTempDir } from "../test-helpers/temp-dir.js" ;
55import { resolveBrewExecutable , resolveBrewPathDirs } from "./brew.js" ;
66
7+ const HOMEBREW_ENV_KEYS = [ "HOMEBREW_BREW_FILE" , "HOMEBREW_PREFIX" ] as const ;
8+
79describe ( "brew helpers" , ( ) => {
810 async function writeExecutable ( filePath : string ) {
911 await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
1012 await fs . writeFile ( filePath , "#!/bin/sh\necho ok\n" , "utf-8" ) ;
1113 await fs . chmod ( filePath , 0o755 ) ;
1214 }
1315
16+ async function withHomebrewEnv (
17+ values : Partial < Record < ( typeof HOMEBREW_ENV_KEYS ) [ number ] , string > > ,
18+ run : ( ) => Promise < void > ,
19+ ) {
20+ const previous = Object . fromEntries (
21+ HOMEBREW_ENV_KEYS . map ( ( key ) => [ key , process . env [ key ] ] ) ,
22+ ) as Record < ( typeof HOMEBREW_ENV_KEYS ) [ number ] , string | undefined > ;
23+ try {
24+ for ( const key of HOMEBREW_ENV_KEYS ) {
25+ const value = values [ key ] ;
26+ if ( value === undefined ) {
27+ delete process . env [ key ] ;
28+ } else {
29+ process . env [ key ] = value ;
30+ }
31+ }
32+ await run ( ) ;
33+ } finally {
34+ for ( const key of HOMEBREW_ENV_KEYS ) {
35+ const value = previous [ key ] ;
36+ if ( value === undefined ) {
37+ delete process . env [ key ] ;
38+ } else {
39+ process . env [ key ] = value ;
40+ }
41+ }
42+ }
43+ }
44+
45+ async function withPathEnv ( value : string | undefined , run : ( ) => Promise < void > ) {
46+ const previous = process . env . PATH ;
47+ try {
48+ if ( value === undefined ) {
49+ delete process . env . PATH ;
50+ } else {
51+ process . env . PATH = value ;
52+ }
53+ await run ( ) ;
54+ } finally {
55+ if ( previous === undefined ) {
56+ delete process . env . PATH ;
57+ } else {
58+ process . env . PATH = previous ;
59+ }
60+ }
61+ }
62+
1463 it ( "resolves brew from ~/.linuxbrew/bin when executable exists" , async ( ) => {
1564 await withTempDir ( { prefix : "openclaw-brew-" } , async ( tmp ) => {
1665 const homebrewBin = path . join ( tmp , ".linuxbrew" , "bin" ) ;
1766 const brewPath = path . join ( homebrewBin , "brew" ) ;
1867 await writeExecutable ( brewPath ) ;
1968
20- const env : NodeJS . ProcessEnv = { } ;
21- expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( brewPath ) ;
69+ await withPathEnv ( "" , async ( ) => {
70+ expect ( resolveBrewExecutable ( { homeDir : tmp } ) ) . toBe ( brewPath ) ;
71+ } ) ;
2272 } ) ;
2373 } ) ;
2474
25- it ( "prefers HOMEBREW_PREFIX/bin/ brew when present " , async ( ) => {
75+ it ( "resolves brew from absolute PATH entries for non-standard installs " , async ( ) => {
2676 await withTempDir ( { prefix : "openclaw-brew-" } , async ( tmp ) => {
27- const prefix = path . join ( tmp , "prefix" ) ;
28- const prefixBin = path . join ( prefix , "bin" ) ;
29- const prefixBrew = path . join ( prefixBin , "brew" ) ;
30- await writeExecutable ( prefixBrew ) ;
31-
32- const homebrewBin = path . join ( tmp , ".linuxbrew" , "bin" ) ;
33- const homebrewBrew = path . join ( homebrewBin , "brew" ) ;
34- await writeExecutable ( homebrewBrew ) ;
77+ const customBin = path . join ( tmp , "custom-homebrew" , "bin" ) ;
78+ const customBrew = path . join ( customBin , "brew" ) ;
79+ await writeExecutable ( customBrew ) ;
3580
36- const env : NodeJS . ProcessEnv = { HOMEBREW_PREFIX : prefix } ;
37- expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( prefixBrew ) ;
81+ await withPathEnv ( customBin , async ( ) => {
82+ expect ( resolveBrewExecutable ( { homeDir : path . join ( tmp , "home" ) } ) ) . toBe ( customBrew ) ;
83+ } ) ;
3884 } ) ;
3985 } ) ;
4086
41- it ( "prefers HOMEBREW_BREW_FILE over prefix and trims value " , async ( ) => {
87+ it ( "ignores HOMEBREW_BREW_FILE and HOMEBREW_PREFIX by default " , async ( ) => {
4288 await withTempDir ( { prefix : "openclaw-brew-" } , async ( tmp ) => {
4389 const explicit = path . join ( tmp , "custom" , "brew" ) ;
4490 const prefix = path . join ( tmp , "prefix" ) ;
45- const prefixBrew = path . join ( prefix , "bin" , "brew" ) ;
91+ const prefixBin = path . join ( prefix , "bin" ) ;
92+ const prefixBrew = path . join ( prefixBin , "brew" ) ;
93+ const homebrewBin = path . join ( tmp , ".linuxbrew" , "bin" ) ;
94+ const homebrewBrew = path . join ( homebrewBin , "brew" ) ;
4695 await writeExecutable ( explicit ) ;
4796 await writeExecutable ( prefixBrew ) ;
97+ await writeExecutable ( homebrewBrew ) ;
4898
49- const env : NodeJS . ProcessEnv = {
50- HOMEBREW_BREW_FILE : ` ${ explicit } ` ,
51- HOMEBREW_PREFIX : prefix ,
52- } ;
53- expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( explicit ) ;
54- } ) ;
55- } ) ;
56-
57- it ( "falls back to prefix when HOMEBREW_BREW_FILE is missing or not executable" , async ( ) => {
58- await withTempDir ( { prefix : "openclaw-brew-" } , async ( tmp ) => {
59- const explicit = path . join ( tmp , "custom" , "brew" ) ;
60- const prefix = path . join ( tmp , "prefix" ) ;
61- const prefixBrew = path . join ( prefix , "bin" , "brew" ) ;
62- let brewFile = explicit ;
63- if ( process . platform === "win32" ) {
64- // Windows doesn't enforce POSIX executable bits, so use a missing path
65- // to verify fallback behavior deterministically.
66- brewFile = path . join ( tmp , "custom" , "missing-brew" ) ;
67- } else {
68- await fs . mkdir ( path . dirname ( explicit ) , { recursive : true } ) ;
69- await fs . writeFile ( explicit , "#!/bin/sh\necho no\n" , "utf-8" ) ;
70- await fs . chmod ( explicit , 0o644 ) ;
71- }
72- await writeExecutable ( prefixBrew ) ;
73-
74- const env : NodeJS . ProcessEnv = {
75- HOMEBREW_BREW_FILE : brewFile ,
76- HOMEBREW_PREFIX : prefix ,
77- } ;
78- expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( prefixBrew ) ;
99+ await withHomebrewEnv (
100+ {
101+ HOMEBREW_BREW_FILE : explicit ,
102+ HOMEBREW_PREFIX : prefix ,
103+ } ,
104+ async ( ) => {
105+ const env : NodeJS . ProcessEnv = {
106+ HOMEBREW_BREW_FILE : explicit ,
107+ HOMEBREW_PREFIX : prefix ,
108+ } ;
109+ await withPathEnv ( "" , async ( ) => {
110+ expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( homebrewBrew ) ;
111+ } ) ;
112+ expect ( resolveBrewPathDirs ( { homeDir : tmp , env } ) ) . not . toContain ( prefixBin ) ;
113+ } ,
114+ ) ;
79115 } ) ;
80116 } ) ;
81117
@@ -85,37 +121,39 @@ describe("brew helpers", () => {
85121 const brewPath = path . join ( homebrewBin , "brew" ) ;
86122 await writeExecutable ( brewPath ) ;
87123
88- const env : NodeJS . ProcessEnv = {
89- HOMEBREW_BREW_FILE : " " ,
90- HOMEBREW_PREFIX : "\t" ,
91- } ;
92-
93- expect ( resolveBrewExecutable ( { homeDir : tmp , env } ) ) . toBe ( brewPath ) ;
94-
95- const dirs = resolveBrewPathDirs ( { homeDir : tmp , env } ) ;
96- expect ( dirs ) . not . toContain ( path . join ( "" , "bin" ) ) ;
97- expect ( dirs ) . not . toContain ( path . join ( "" , "sbin" ) ) ;
124+ await withHomebrewEnv (
125+ {
126+ HOMEBREW_BREW_FILE : " " ,
127+ HOMEBREW_PREFIX : "\t" ,
128+ } ,
129+ async ( ) => {
130+ await withPathEnv ( "" , async ( ) => {
131+ expect ( resolveBrewExecutable ( { homeDir : tmp } ) ) . toBe ( brewPath ) ;
132+ } ) ;
133+
134+ const dirs = resolveBrewPathDirs ( { homeDir : tmp } ) ;
135+ expect ( dirs ) . not . toContain ( path . join ( "" , "bin" ) ) ;
136+ expect ( dirs ) . not . toContain ( path . join ( "" , "sbin" ) ) ;
137+ } ,
138+ ) ;
98139 } ) ;
99140 } ) ;
100141
101142 it ( "always includes the standard macOS brew dirs after linuxbrew candidates" , ( ) => {
102- const env : NodeJS . ProcessEnv = {
103- HOMEBREW_BREW_FILE : " " ,
104- HOMEBREW_PREFIX : " " ,
105- } ;
106- const dirs = resolveBrewPathDirs ( { homeDir : "/home/test" , env } ) ;
143+ const dirs = resolveBrewPathDirs ( { homeDir : "/home/test" } ) ;
107144
108145 expect ( dirs . slice ( - 2 ) ) . toEqual ( [ "/opt/homebrew/bin" , "/usr/local/bin" ] ) ;
109146 } ) ;
110147
111- it ( "includes Linuxbrew bin/sbin in path candidates" , ( ) => {
112- const env : NodeJS . ProcessEnv = { HOMEBREW_PREFIX : "/custom/prefix" } ;
113- const dirs = resolveBrewPathDirs ( { homeDir : "/home/test" , env } ) ;
114- expect ( dirs ) . toContain ( path . join ( "/custom/prefix" , "bin" ) ) ;
115- expect ( dirs ) . toContain ( path . join ( "/custom/prefix" , "sbin" ) ) ;
116- expect ( dirs ) . toContain ( "/home/linuxbrew/.linuxbrew/bin" ) ;
117- expect ( dirs ) . toContain ( "/home/linuxbrew/.linuxbrew/sbin" ) ;
118- expect ( dirs ) . toContain ( path . join ( "/home/test" , ".linuxbrew" , "bin" ) ) ;
119- expect ( dirs ) . toContain ( path . join ( "/home/test" , ".linuxbrew" , "sbin" ) ) ;
148+ it ( "includes Linuxbrew bin/sbin in path candidates without env prefixes" , async ( ) => {
149+ await withHomebrewEnv ( { HOMEBREW_PREFIX : "/custom/prefix" } , async ( ) => {
150+ const dirs = resolveBrewPathDirs ( { homeDir : "/home/test" } ) ;
151+ expect ( dirs ) . not . toContain ( path . join ( "/custom/prefix" , "bin" ) ) ;
152+ expect ( dirs ) . not . toContain ( path . join ( "/custom/prefix" , "sbin" ) ) ;
153+ expect ( dirs ) . toContain ( "/home/linuxbrew/.linuxbrew/bin" ) ;
154+ expect ( dirs ) . toContain ( "/home/linuxbrew/.linuxbrew/sbin" ) ;
155+ expect ( dirs ) . toContain ( path . join ( "/home/test" , ".linuxbrew" , "bin" ) ) ;
156+ expect ( dirs ) . toContain ( path . join ( "/home/test" , ".linuxbrew" , "sbin" ) ) ;
157+ } ) ;
120158 } ) ;
121159} ) ;
0 commit comments