mirror of https://github.com/garrytan/gstack.git
test(setup): guard plan-tune hooks stay non-interactive
Static + binary-level regression test (free, <1s): asserts the flags are wired, the plan-tune read is time-bounded (no bare blocking read), explicit yes/no decisions short-circuit before the prompt, and gstack-config knows the plan_tune_hooks key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1576a8054d
commit
352f6a571b
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// Regression guard for the conductor/workspace setup hang:
|
||||
// `./setup` used a blocking `read -r` to ask "Install both hooks now? [y/N]".
|
||||
// When setup runs under a forwarded/automated TTY (conductor workspace setup,
|
||||
// CI with a pty) the read blocked forever. The fix moves the decision into
|
||||
// flags + env + saved config with a non-blocking, time-bounded prompt fallback.
|
||||
//
|
||||
// These are static + binary-level assertions (free, <1s) — they lock in the
|
||||
// contract without running the full (environment-mutating) setup script.
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SETUP = path.join(ROOT, 'setup');
|
||||
const GSTACK_CONFIG = path.join(ROOT, 'bin', 'gstack-config');
|
||||
|
||||
const setupSrc = fs.readFileSync(SETUP, 'utf-8');
|
||||
|
||||
describe('setup: plan-tune hooks are non-interactive-safe', () => {
|
||||
test('exposes --plan-tune-hooks / --no-plan-tune-hooks / =value flags', () => {
|
||||
expect(setupSrc).toContain('--plan-tune-hooks)');
|
||||
expect(setupSrc).toContain('--no-plan-tune-hooks)');
|
||||
expect(setupSrc).toContain('--plan-tune-hooks=*)');
|
||||
});
|
||||
|
||||
test('resolution falls through env then saved config', () => {
|
||||
expect(setupSrc).toContain('GSTACK_PLAN_TUNE_HOOKS');
|
||||
expect(setupSrc).toContain('get plan_tune_hooks');
|
||||
});
|
||||
|
||||
test('explicit yes/no decisions never reach a prompt', () => {
|
||||
// The yes/no branches must short-circuit before the interactive branch.
|
||||
const yesIdx = setupSrc.indexOf('PT_DECISION" = "yes"');
|
||||
const noIdx = setupSrc.indexOf('PT_DECISION" = "no"');
|
||||
const promptIdx = setupSrc.indexOf('Install both hooks now?');
|
||||
expect(yesIdx).toBeGreaterThan(-1);
|
||||
expect(noIdx).toBeGreaterThan(-1);
|
||||
expect(yesIdx).toBeLessThan(promptIdx);
|
||||
expect(noIdx).toBeLessThan(promptIdx);
|
||||
});
|
||||
|
||||
test('the interactive prompt is time-bounded (cannot hang)', () => {
|
||||
// No bare blocking read for the plan-tune reply.
|
||||
expect(setupSrc).not.toMatch(/read -r PLAN_TUNE_INSTALL_REPLY\b/);
|
||||
// It must use a timed read from the controlling tty with an empty fallback.
|
||||
expect(setupSrc).toMatch(/read -t \d+ -r PLAN_TUNE_INSTALL_REPLY <\/dev\/tty/);
|
||||
});
|
||||
|
||||
test('interactive prompt is gated on a real TTY and non-quiet', () => {
|
||||
// The prompt branch requires both stdin+stdout TTYs and not --quiet.
|
||||
expect(setupSrc).toMatch(/\[ "\$QUIET" -ne 1 \] && \[ -t 0 \] && \[ -t 1 \]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gstack-config: plan_tune_hooks key', () => {
|
||||
test('default is "prompt"', () => {
|
||||
const out = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, {
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, HOME: process.env.HOME },
|
||||
}).trim();
|
||||
expect(out).toBe('prompt');
|
||||
});
|
||||
|
||||
test('appears in defaults and list output', () => {
|
||||
const defaults = execSync(`${GSTACK_CONFIG} defaults`, { encoding: 'utf-8' });
|
||||
expect(defaults).toContain('plan_tune_hooks');
|
||||
const list = execSync(`${GSTACK_CONFIG} list`, { encoding: 'utf-8' });
|
||||
expect(list).toContain('plan_tune_hooks');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue