mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 16:34:00 +01:00
refactor(capy-install): write_launch_file quotes through shq; header names the launch file
Review finding: _sq duplicated shq() from the same file. The launch file's assignments now go through shq, and the header comment describes the GSTACK_LAUNCH_PATH and GSTACK_LAUNCH_FILE lines beside GSTACK_LAUNCH. Step 7 coverage: the verdict test sources the launch file and runs --check; the single-quote path test sources the file too; a new case makes the launch-file directory unwritable and checks the run says so, prints no GSTACK_LAUNCH_FILE line, leaves no temp file and keeps its exit code and trailer (linux, non-root). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
58f456acb1
commit
9e5ccbb6eb
2 files changed
+64
-8
No files matched your search
@@ -34,7 +34,10 @@
|
||||
# Chromium is lazy by default (bin/gstack-browser-ensure
|
||||
# installs it on first use); --with-browser installs it now. The last lines
|
||||
# are the GSTACK_LAUNCH line the parent supplies to every gstack command it
|
||||
# launches and the doctor's trailer (`gstack: ok ...` or `gstack: fail ...`;
|
||||
# launches, GSTACK_LAUNCH_PATH when the certified Bun is not first on PATH,
|
||||
# GSTACK_LAUNCH_FILE (the same assignments as a sourceable mode-0600 file
|
||||
# beside the verdict; callers and their children `. <file>` it, never re-type
|
||||
# it) and the doctor's trailer (`gstack: ok ...` or `gstack: fail ...`;
|
||||
# grep that, never pipe through tail).
|
||||
# Exit: 0 ok, 1 a component failed, 2 usage, 3 preflight stopped before ./setup.
|
||||
set -u
|
||||
@@ -586,21 +589,20 @@ say "total: $(( $(now) - T0 ))s"
|
||||
# ran a bare `export` when the substitution came back empty and dumped the
|
||||
# whole environment, API keys included, into its tool output (GSTA-96).
|
||||
LAUNCH_FILE="${VERDICT%.json}.env"
|
||||
_sq() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
||||
write_launch_file() {
|
||||
local tmp
|
||||
mkdir -p "$(dirname "$LAUNCH_FILE")" || return 1
|
||||
tmp=$(umask 077 && mktemp "$LAUNCH_FILE.tmp.XXXXXX") || return 1
|
||||
{
|
||||
echo "# gstack launch environment, written by gstack-capy-install; mode 0600."
|
||||
echo "# Apply it with: . $(_sq "$LAUNCH_FILE")"
|
||||
echo "# Apply it with: . $(shq "$LAUNCH_FILE")"
|
||||
echo "# Never print it or the environment: no \`export \$(...)\`, \`env\`, \`printenv\` or bare \`set\` in a tool fence."
|
||||
[ -n "$LAUNCH_DIR" ] && echo "case \":\$PATH:\" in *:$(_sq "$LAUNCH_DIR"):*) ;; *) PATH=$(_sq "$LAUNCH_DIR"):\"\$PATH\" ;; esac; export PATH"
|
||||
[ -n "$LAUNCH_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$LAUNCH_DIR"):*) ;; *) PATH=$(shq "$LAUNCH_DIR"):\"\$PATH\" ;; esac; export PATH"
|
||||
echo "export GSTACK_SESSION_KIND=unattended"
|
||||
echo "export GSTACK_STATE_ROOT=$(_sq "$GSTACK_STATE_ROOT")"
|
||||
echo "export GSTACK_EPHEMERAL=$(_sq "${GSTACK_EPHEMERAL:-1}")"
|
||||
echo "export GSTACK_STATE_ROOT=$(shq "$GSTACK_STATE_ROOT")"
|
||||
echo "export GSTACK_EPHEMERAL=$(shq "${GSTACK_EPHEMERAL:-1}")"
|
||||
echo "export GSTACK_CAPY=1"
|
||||
echo "export GSTACK_INSTALL_VERDICT=$(_sq "$VERDICT")"
|
||||
echo "export GSTACK_INSTALL_VERDICT=$(shq "$VERDICT")"
|
||||
} > "$tmp" && chmod 0600 "$tmp" && mv -f "$tmp" "$LAUNCH_FILE"
|
||||
}
|
||||
write_launch_file || { rm -f "$LAUNCH_FILE".tmp.* 2>/dev/null; say "could not write the launch file $LAUNCH_FILE"; LAUNCH_FILE=""; }
|
||||
|
||||
@@ -423,6 +423,16 @@ describe('gstack-capy-install: launch PATH, Bun reuse, CI pins, setup skip, verd
|
||||
expect(r.stdout).toContain(`GSTACK_INSTALL_VERDICT=${files[0]}`);
|
||||
expect(r.stdout).toContain(`GSTACK_LAUNCH_FILE: ${files[0].replace(/\.json$/, '.env')}`);
|
||||
expect(fs.readFileSync(files[0].replace(/\.json$/, '.env'), 'utf8')).toContain(`export GSTACK_INSTALL_VERDICT='${files[0]}'`);
|
||||
// /ship Step 7 value card. protects: with the certified Bun first on PATH (no GSTACK_LAUNCH_PATH)
|
||||
// the launch file carries no PATH line, sources under POSIX sh, and leaves PATH exactly as it was;
|
||||
// --check rewrites and prints it too. fails_when: the PATH guard is dropped (an empty prepend
|
||||
// leaves ":"-led PATH), the file uses a bashism, or --check stops printing GSTACK_LAUNCH_FILE.
|
||||
// why_existing_misses: the PATH-bearing case above is sourced by bash only, and the --check run
|
||||
// below asserted only the GSTACK_LAUNCH line. seam=none.
|
||||
const sourced = spawnSync('sh', ['-c', '. "$1" && printf "%s\\n" "$PATH" "$GSTACK_SESSION_KIND" "$GSTACK_CAPY" "$GSTACK_INSTALL_VERDICT"', '_', files[0].replace(/\.json$/, '.env')],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: '/usr/bin:/bin' } });
|
||||
expect(sourced.status, sourced.stderr).toBe(0);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual(['/usr/bin:/bin', 'unattended', '1', files[0]]);
|
||||
const v = JSON.parse(fs.readFileSync(files[0], 'utf8'));
|
||||
expect(v).toMatchObject({ schema_version: 2, checkout: f.root, head: sh('git rev-parse HEAD', f.root).stdout.trim(), for: 'autoplan', ok: true, trailer: 'gstack: ok 1.2.3 for=autoplan',
|
||||
project: '', bun_path: path.join(f.stub, 'bun'), bun_version: '1.4.2', bun_rule: 'PATH' });
|
||||
@@ -433,11 +443,40 @@ describe('gstack-capy-install: launch PATH, Bun reuse, CI pins, setup skip, verd
|
||||
expect(c.status, c.stdout + c.stderr).toBe(0);
|
||||
expect(f.calls().filter(l => l.startsWith('setup ')).length).toBe(setups);
|
||||
expect(c.stdout).toContain('GSTACK_LAUNCH: ');
|
||||
expect(c.stdout).toContain(`GSTACK_LAUNCH_FILE: ${files[0].replace(/\.json$/, '.env')}`);
|
||||
expect(lastLine(c.stdout)).toBe('gstack: ok 1.2.3 for=autoplan');
|
||||
const failed = run(f, ['--check', '--for', 'autoplan'], { STUB_DOCTOR_FAIL: '1' });
|
||||
expect(failed.status).toBe(1);
|
||||
expect(JSON.parse(fs.readFileSync(files[0], 'utf8'))).toMatchObject({ ok: false, trailer: 'gstack: fail codex' });
|
||||
});
|
||||
|
||||
// /ship Step 7 value card. protects: when the launch file cannot be written, the run says so,
|
||||
// prints no GSTACK_LAUNCH_FILE line (a caller must never be handed an empty path to source),
|
||||
// leaves no temp file behind, and keeps its exit code and trailer; the earlier launch file is
|
||||
// untouched. fails_when: the `[ -n "$LAUNCH_FILE" ]` guard goes, the write failure changes RC or
|
||||
// ordering, or the temp file is created outside the umask/cleanup path. why_existing_misses:
|
||||
// every fixture's state root is writable. seam=none (filesystem permissions). no_claim: root.
|
||||
test.skipIf(process.platform !== 'linux' || process.getuid?.() === 0)('a launch file that cannot be written is reported, prints no GSTACK_LAUNCH_FILE line, leaves no temp file and keeps the exit code and trailer', () => {
|
||||
const f = fixture();
|
||||
const first = run(f, ['--for', 'autoplan']);
|
||||
expect(first.status, first.stdout + first.stderr).toBe(0);
|
||||
const launchFile = first.stdout.match(/^GSTACK_LAUNCH_FILE: (.+)$/m)?.[1] ?? '';
|
||||
const before = fs.readFileSync(launchFile, 'utf8');
|
||||
const dir = path.dirname(launchFile);
|
||||
fs.chmodSync(dir, 0o555);
|
||||
try {
|
||||
const c = run(f, ['--check', '--for', 'autoplan']);
|
||||
expect(c.status, c.stdout + c.stderr).toBe(0);
|
||||
expect(c.stdout).toContain(`gstack-capy-install: could not write the launch file ${launchFile}`);
|
||||
expect(c.stdout).toContain('GSTACK_LAUNCH: ');
|
||||
expect(c.stdout).not.toMatch(/^GSTACK_LAUNCH_FILE:/m);
|
||||
expect(lastLine(c.stdout)).toBe('gstack: ok 1.2.3 for=autoplan');
|
||||
expect(fs.readdirSync(dir).filter(n => n.includes('.tmp.'))).toEqual([]);
|
||||
expect(fs.readFileSync(launchFile, 'utf8')).toBe(before);
|
||||
} finally {
|
||||
fs.chmodSync(dir, 0o755);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Plan A1/A3/A5/A6/A-f: install and --check resolve through one contract,
|
||||
@@ -771,8 +810,23 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
fs.mkdirSync(path.join(f.home, '.capy/system'), { recursive: true });
|
||||
const state = path.join(f.base, "it's state");
|
||||
realBun(path.join(state, `runtimes/bun/${BUN_DEFAULT}/bun`), BUN_DEFAULT);
|
||||
expect(run(f, ['--for', 'autoplan'], { GSTACK_STATE_ROOT: state }, f.base).status).toBe(0);
|
||||
const installed = run(f, ['--for', 'autoplan'], { GSTACK_STATE_ROOT: state, GSTACK_EPHEMERAL: '0' }, f.base);
|
||||
expect(installed.status, installed.stdout + installed.stderr).toBe(0);
|
||||
expect(sh(`sh -n "${f.home}/.local/bin/bun"`).status).toBe(0);
|
||||
// /ship Step 7 value card. protects: the launch file single-quotes every path it bakes in, so a
|
||||
// state root, launch directory and verdict path holding a quote and a space source under POSIX
|
||||
// sh (twice: the PATH prepend stays idempotent) and come back byte-identical, and the file
|
||||
// carries the caller's GSTACK_EPHEMERAL rather than a hard-coded 1. fails_when: _sq is replaced
|
||||
// by plain interpolation or double quotes, the case pattern loses its quoting, or GSTACK_EPHEMERAL
|
||||
// is hard-coded. why_existing_misses: the sourced launch files above live under plain tmp paths
|
||||
// with the default GSTACK_EPHEMERAL. seam=none.
|
||||
const launchFile = installed.stdout.match(/^GSTACK_LAUNCH_FILE: (.+)$/m)?.[1] ?? '';
|
||||
expect(launchFile.startsWith(`${state}/install-verdicts/`), launchFile).toBe(true);
|
||||
expect(installed.stdout).toContain(`GSTACK_LAUNCH_PATH: ${state}/runtimes/bun/${BUN_DEFAULT}`);
|
||||
const sourced = spawnSync('sh', ['-c', '. "$1" && . "$1" && printf "%s\\n" "$PATH" "$GSTACK_STATE_ROOT" "$GSTACK_EPHEMERAL" "$GSTACK_INSTALL_VERDICT"', '_', launchFile],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: '/usr/bin:/bin' } });
|
||||
expect(sourced.status, sourced.stderr).toBe(0);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual([`${state}/runtimes/bun/${BUN_DEFAULT}:/usr/bin:/bin`, state, '0', launchFile.replace(/\.env$/, '.json')]);
|
||||
const r = fresh(f, f.base, 'bun --version && bunx --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout.trim().split('\n')[0]).toBe(BUN_DEFAULT);
|
||||
|
||||
Reference in new issue
Block a user