diff --git a/test/unit/selfhost-deploy-common.test.ts b/test/unit/selfhost-deploy-common.test.ts index 61a6719483..c59ef980ac 100644 --- a/test/unit/selfhost-deploy-common.test.ts +++ b/test/unit/selfhost-deploy-common.test.ts @@ -227,3 +227,86 @@ printf 'REACHED_END args=[%s]\\n' "\${compose_args[*]}" expect(result.stderr).toContain("compose file not found: missing.yml"); }); }); + +// Generic seam for the remaining library functions (#7769): source the lib in a scratch dir, invoke one +// function with the given args, and return its status/stdout/stderr -- the same spawn-a-real-bash approach as +// createHarness above, but parameterized so env_get/require_cmd can each be driven directly. +// (compose_file_args is already covered by the #7765 suite above.) +function runLibFn(call: string, options: { env?: Record; files?: Record } = {}) { + const dir = mkdtempSync(join(tmpdir(), "loopover-selfhost-deploy-common-fn-")); + try { + for (const [name, contents] of Object.entries(options.files ?? {})) { + writeFileSync(join(dir, name), contents); + } + const scriptPath = join(dir, "run.sh"); + // set -u is deliberately NOT enabled: env_get reads optional ENV_FILE with `${VAR:-default}` guards, + // exactly as the real deploy scripts do. + writeFileSync(scriptPath, `#!/usr/bin/env bash\nset -eo pipefail\n. "${libPath.replace(/\\/g, "/")}"\n${call}\n`); + chmodSync(scriptPath, 0o755); + return spawnSync("bash", [scriptPath], { + cwd: dir, + encoding: "utf8", + env: { ...process.env, ...(options.env ?? {}) }, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +describe("require_cmd (#7769)", () => { + it("succeeds silently when the command exists", () => { + const result = runLibFn("require_cmd bash"); + expect(result.status, result.stderr).toBe(0); + expect(result.stderr).toBe(""); + }); + + it("fails with exit 1 and a clear error when the command is missing", () => { + const result = runLibFn("require_cmd loopover-definitely-not-a-real-command"); + expect(result.status).toBe(1); + expect(result.stderr).toContain("required command not found: loopover-definitely-not-a-real-command"); + }); +}); + +describe("env_get (#7769)", () => { + it("returns a plain unquoted value for a matching key", () => { + const result = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "FOO=bar\n" } }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe("bar\n"); + }); + + it("strips surrounding double and single quotes from the value", () => { + const dq = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": 'FOO="quoted value"\n' } }); + expect(dq.status, dq.stderr).toBe(0); + expect(dq.stdout).toBe("quoted value\n"); + + const sq = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "FOO='single quoted'\n" } }); + expect(sq.status, sq.stderr).toBe(0); + expect(sq.stdout).toBe("single quoted\n"); + }); + + it("skips comment and blank lines and returns the first matching key", () => { + const result = runLibFn('env_get FOO "$PWD/.env"', { files: { ".env": "# a comment\n\n FOO = spaced\nFOO=second\n" } }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe("spaced\n"); + }); + + it("returns exit 1 when the key is absent from the file", () => { + const result = runLibFn('env_get MISSING "$PWD/.env"', { files: { ".env": "FOO=bar\n" } }); + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + }); + + it("returns exit 1 when the file does not exist", () => { + const result = runLibFn('env_get FOO "$PWD/does-not-exist.env"'); + expect(result.status).toBe(1); + }); + + it("falls back to $ENV_FILE when no file argument is given", () => { + const result = runLibFn("env_get FOO", { + files: { ".env": "FOO=from-env-file\n" }, + env: { ENV_FILE: ".env" }, + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toBe("from-env-file\n"); + }); +});