From f5f7e325055aa4ace547ed0fe324f686e78a5ae0 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 20:07:42 -0700 Subject: [PATCH] feat: add shared call-time isConductor() helper Single source of truth for Conductor host detection in TS consumers (CONDUCTOR_WORKSPACE_PATH / CONDUCTOR_PORT). Reads the passed env at call time, not a module-load snapshot, so unit tests can pin the env inline without Bun --preload (esm-hoist-breaks-env-pin-bootstrap). Co-Authored-By: Claude Fable 5 --- lib/is-conductor.ts | 19 +++++++++++++++ test/is-conductor.test.ts | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 lib/is-conductor.ts create mode 100644 test/is-conductor.test.ts diff --git a/lib/is-conductor.ts b/lib/is-conductor.ts new file mode 100644 index 000000000..3f31c88af --- /dev/null +++ b/lib/is-conductor.ts @@ -0,0 +1,19 @@ +/** + * Conductor host detection — single source of truth for TS consumers. + * + * Conductor (the Mac app that runs many coding agents in parallel) sets + * CONDUCTOR_WORKSPACE_PATH / CONDUCTOR_PORT in the session env. The same two + * vars are what `bin/gstack-session-kind` keys on (it collapses Conductor into + * `interactive`, so it can't be reused to distinguish Conductor specifically — + * hence this dedicated helper). + * + * IMPORTANT: detection is a CALL-TIME read of the passed-in env (default + * `process.env`), never a module-load-time snapshot. ESM hoists static imports + * above any in-file `process.env.X = ...`, so a load-time read can't be pinned + * by a test without Bun --preload. Reading at call time lets unit tests set + * `process.env.CONDUCTOR_WORKSPACE_PATH` inline before invoking. See the + * `esm-hoist-breaks-env-pin-bootstrap` learning. + */ +export function isConductor(env: NodeJS.ProcessEnv = process.env): boolean { + return !!(env.CONDUCTOR_WORKSPACE_PATH || env.CONDUCTOR_PORT); +} diff --git a/test/is-conductor.test.ts b/test/is-conductor.test.ts new file mode 100644 index 000000000..be06eabe4 --- /dev/null +++ b/test/is-conductor.test.ts @@ -0,0 +1,50 @@ +import { describe, test, expect } from 'bun:test'; +import { isConductor } from '../lib/is-conductor'; + +describe('is-conductor', () => { + test('true when CONDUCTOR_WORKSPACE_PATH is set', () => { + expect(isConductor({ CONDUCTOR_WORKSPACE_PATH: '/Users/x/conductor/ws' })).toBe(true); + }); + + test('true when CONDUCTOR_PORT is set', () => { + expect(isConductor({ CONDUCTOR_PORT: '55070' })).toBe(true); + }); + + test('true when both are set', () => { + expect(isConductor({ CONDUCTOR_WORKSPACE_PATH: '/ws', CONDUCTOR_PORT: '55070' })).toBe(true); + }); + + test('false when neither is set', () => { + expect(isConductor({ HOME: '/Users/x', PATH: '/usr/bin' })).toBe(false); + }); + + test('false on an empty env', () => { + expect(isConductor({})).toBe(false); + }); + + test('false when the vars are present but empty (Codex #1 hardening — empty != set)', () => { + expect(isConductor({ CONDUCTOR_WORKSPACE_PATH: '', CONDUCTOR_PORT: '' })).toBe(false); + }); + + test('reads the passed env at call time, not a module-load snapshot', () => { + const env: NodeJS.ProcessEnv = {}; + expect(isConductor(env)).toBe(false); + // mutate AFTER the first call — a call-time read must see the new value + env.CONDUCTOR_PORT = '55070'; + expect(isConductor(env)).toBe(true); + }); + + test('defaults to process.env when no arg is passed', () => { + const saved = process.env.CONDUCTOR_PORT; + try { + process.env.CONDUCTOR_PORT = '12345'; + expect(isConductor()).toBe(true); + delete process.env.CONDUCTOR_PORT; + // CONDUCTOR_WORKSPACE_PATH may be set in a real Conductor session; guard the assertion + if (!process.env.CONDUCTOR_WORKSPACE_PATH) expect(isConductor()).toBe(false); + } finally { + if (saved === undefined) delete process.env.CONDUCTOR_PORT; + else process.env.CONDUCTOR_PORT = saved; + } + }); +});