/** * gstack-config explain_level round-trip + validation tests. * * Coverage: * - `set explain_level default` persists, `get` returns "default" * - `set explain_level terse` persists, `get` returns "terse" * - `set explain_level garbage` warns + writes "default" * - `get explain_level` with unset key returns empty (preamble bash defaults) * - Annotated config header documents explain_level */ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { spawnSync } from 'child_process'; const ROOT = path.resolve(import.meta.dir, '..'); const BIN_CONFIG = path.join(ROOT, 'bin', 'gstack-config'); let tmpHome: string; beforeEach(() => { tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-cfg-test-')); }); afterEach(() => { fs.rmSync(tmpHome, { recursive: true, force: true }); }); function run(...args: string[]): { stdout: string; stderr: string; status: number } { // gstack-config precedence is `${GSTACK_HOME:-${GSTACK_STATE_DIR:-$HOME/.gstack}}`, // so GSTACK_HOME from the developer's parent env wins over the test's // GSTACK_STATE_DIR. Override both to isolate from the real ~/.gstack. const res = spawnSync(BIN_CONFIG, args, { env: { ...process.env, GSTACK_STATE_DIR: tmpHome, GSTACK_HOME: tmpHome }, encoding: 'utf-8', cwd: ROOT, }); return { stdout: (res.stdout ?? '').trim(), stderr: (res.stderr ?? '').trim(), status: res.status ?? -1, }; } describe('gstack-config explain_level', () => { test('set + get default round-trip', () => { expect(run('set', 'explain_level', 'default').status).toBe(0); expect(run('get', 'explain_level').stdout).toBe('default'); }); test('set + get terse round-trip', () => { expect(run('set', 'explain_level', 'terse').status).toBe(0); expect(run('get', 'explain_level').stdout).toBe('terse'); }); test('unknown value warns and defaults to default', () => { const result = run('set', 'explain_level', 'garbage'); expect(result.status).toBe(0); expect(result.stderr).toContain('not recognized'); expect(result.stderr).toContain('default, terse'); expect(run('get', 'explain_level').stdout).toBe('default'); }); test('get with unset explain_level returns the documented default', () => { // gstack-config returns the documented default ("default") when the // key is absent from config.yaml — see bin/gstack-config:103. Earlier // versions of this test expected "" (preamble shell substitution), // but the script ships defaults inline so callers always get a // usable value without bash fallback gymnastics. expect(run('get', 'explain_level').stdout).toBe('default'); }); test('config header documents explain_level', () => { // Trigger file creation with any set run('set', 'explain_level', 'default'); const cfg = fs.readFileSync(path.join(tmpHome, 'config.yaml'), 'utf-8'); expect(cfg).toContain('explain_level'); expect(cfg).toContain('default'); expect(cfg).toContain('terse'); }); test('set terse, then set garbage restores default', () => { run('set', 'explain_level', 'terse'); expect(run('get', 'explain_level').stdout).toBe('terse'); const garbage = run('set', 'explain_level', 'nonsense'); expect(garbage.stderr).toContain('not recognized'); expect(run('get', 'explain_level').stdout).toBe('default'); }); });