@@ -227,3 +227,86 @@ printf 'REACHED_END args=[%s]\\n' "\${compose_args[*]}"
227227 expect ( result . stderr ) . toContain ( "compose file not found: missing.yml" ) ;
228228 } ) ;
229229} ) ;
230+
231+ // Generic seam for the remaining library functions (#7769): source the lib in a scratch dir, invoke one
232+ // function with the given args, and return its status/stdout/stderr -- the same spawn-a-real-bash approach as
233+ // createHarness above, but parameterized so env_get/require_cmd can each be driven directly.
234+ // (compose_file_args is already covered by the #7765 suite above.)
235+ function runLibFn ( call : string , options : { env ?: Record < string , string > ; files ?: Record < string , string > } = { } ) {
236+ const dir = mkdtempSync ( join ( tmpdir ( ) , "loopover-selfhost-deploy-common-fn-" ) ) ;
237+ try {
238+ for ( const [ name , contents ] of Object . entries ( options . files ?? { } ) ) {
239+ writeFileSync ( join ( dir , name ) , contents ) ;
240+ }
241+ const scriptPath = join ( dir , "run.sh" ) ;
242+ // set -u is deliberately NOT enabled: env_get reads optional ENV_FILE with `${VAR:-default}` guards,
243+ // exactly as the real deploy scripts do.
244+ writeFileSync ( scriptPath , `#!/usr/bin/env bash\nset -eo pipefail\n. "${ libPath . replace ( / \\ / g, "/" ) } "\n${ call } \n` ) ;
245+ chmodSync ( scriptPath , 0o755 ) ;
246+ return spawnSync ( "bash" , [ scriptPath ] , {
247+ cwd : dir ,
248+ encoding : "utf8" ,
249+ env : { ...process . env , ...( options . env ?? { } ) } ,
250+ } ) ;
251+ } finally {
252+ rmSync ( dir , { recursive : true , force : true } ) ;
253+ }
254+ }
255+
256+ describe ( "require_cmd (#7769)" , ( ) => {
257+ it ( "succeeds silently when the command exists" , ( ) => {
258+ const result = runLibFn ( "require_cmd bash" ) ;
259+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
260+ expect ( result . stderr ) . toBe ( "" ) ;
261+ } ) ;
262+
263+ it ( "fails with exit 1 and a clear error when the command is missing" , ( ) => {
264+ const result = runLibFn ( "require_cmd loopover-definitely-not-a-real-command" ) ;
265+ expect ( result . status ) . toBe ( 1 ) ;
266+ expect ( result . stderr ) . toContain ( "required command not found: loopover-definitely-not-a-real-command" ) ;
267+ } ) ;
268+ } ) ;
269+
270+ describe ( "env_get (#7769)" , ( ) => {
271+ it ( "returns a plain unquoted value for a matching key" , ( ) => {
272+ const result = runLibFn ( 'env_get FOO "$PWD/.env"' , { files : { ".env" : "FOO=bar\n" } } ) ;
273+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
274+ expect ( result . stdout ) . toBe ( "bar\n" ) ;
275+ } ) ;
276+
277+ it ( "strips surrounding double and single quotes from the value" , ( ) => {
278+ const dq = runLibFn ( 'env_get FOO "$PWD/.env"' , { files : { ".env" : 'FOO="quoted value"\n' } } ) ;
279+ expect ( dq . status , dq . stderr ) . toBe ( 0 ) ;
280+ expect ( dq . stdout ) . toBe ( "quoted value\n" ) ;
281+
282+ const sq = runLibFn ( 'env_get FOO "$PWD/.env"' , { files : { ".env" : "FOO='single quoted'\n" } } ) ;
283+ expect ( sq . status , sq . stderr ) . toBe ( 0 ) ;
284+ expect ( sq . stdout ) . toBe ( "single quoted\n" ) ;
285+ } ) ;
286+
287+ it ( "skips comment and blank lines and returns the first matching key" , ( ) => {
288+ const result = runLibFn ( 'env_get FOO "$PWD/.env"' , { files : { ".env" : "# a comment\n\n FOO = spaced\nFOO=second\n" } } ) ;
289+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
290+ expect ( result . stdout ) . toBe ( "spaced\n" ) ;
291+ } ) ;
292+
293+ it ( "returns exit 1 when the key is absent from the file" , ( ) => {
294+ const result = runLibFn ( 'env_get MISSING "$PWD/.env"' , { files : { ".env" : "FOO=bar\n" } } ) ;
295+ expect ( result . status ) . toBe ( 1 ) ;
296+ expect ( result . stdout ) . toBe ( "" ) ;
297+ } ) ;
298+
299+ it ( "returns exit 1 when the file does not exist" , ( ) => {
300+ const result = runLibFn ( 'env_get FOO "$PWD/does-not-exist.env"' ) ;
301+ expect ( result . status ) . toBe ( 1 ) ;
302+ } ) ;
303+
304+ it ( "falls back to $ENV_FILE when no file argument is given" , ( ) => {
305+ const result = runLibFn ( "env_get FOO" , {
306+ files : { ".env" : "FOO=from-env-file\n" } ,
307+ env : { ENV_FILE : ".env" } ,
308+ } ) ;
309+ expect ( result . status , result . stderr ) . toBe ( 0 ) ;
310+ expect ( result . stdout ) . toBe ( "from-env-file\n" ) ;
311+ } ) ;
312+ } ) ;
0 commit comments