'use strict';
process.env.MSD_TEST_MODE = '1';
/**
* Unit tests for host-runtime-detection.cjs (#3245, epic #2313 Phase 5).
*
* Requires the COMPILED artifact:
* ../msd-core/bin/lib/host-runtime-detection.cjs
*
* detectHostRuntime is a pure, injected-deps function; resolveReportedRuntime
* layers the explicit MSD_RUNTIME/config.json precedence (via runtime-slash's
* resolveExplicitRuntime) above the host-detection rung. Neither writes to
* disk (#2297).
*/
const { test, describe, mock } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const fc = require('fast-check');
const { createTempProject, cleanup } = require('./helpers.cjs');
const LIB_DIR = path.join(__dirname, '..', 'msd-core', 'bin', 'lib');
const {
CODEX_SESSION_ENV_SIGNALS,
CODEX_CONFIG_HOME_ENV,
CODEX_CONFIG_MARKER,
detectHostRuntime,
resolveReportedRuntime,
} = require(path.join(LIB_DIR, 'host-runtime-detection.cjs'));
const { resolveRuntime, resolveExplicitRuntime } = require(path.join(LIB_DIR, 'runtime-slash.cjs'));
const updateContext = require(path.join(LIB_DIR, 'update-context.cjs'));
const NONE_RESULT = { runtime: null, source: 'none', signal: null };
/**
* Returns a fileExists spy pre-seeded with a set of "existing" paths. Every
* probed path is recorded (normalized, backslash -> forward-slash) on
* `.calls`, so a test can assert both the return value AND exactly which
* paths were probed (needed for the negative-probe assertion at #13).
*/
function spyFileExists(existingPaths = []) {
const normalized = new Set(existingPaths.map((p) => String(p).replace(/\\/g, '/')));
const spy = (p) => {
const key = String(p).replace(/\\/g, '/');
spy.calls.push(key);
return normalized.has(key);
};
spy.calls = [];
return spy;
}
/** Write `
/.planning/config.json` with a raw (possibly non-JSON) string. */
function writeConfig(dir, rawString) {
const planningDir = path.join(dir, '.planning');
fs.mkdirSync(planningDir, { recursive: true });
fs.writeFileSync(path.join(planningDir, 'config.json'), rawString);
}
// ─── detectHostRuntime ───────────────────────────────────────────────────
describe('detectHostRuntime', () => {
test('CODEX_SANDBOX detects a codex session', () => {
const result = detectHostRuntime({ env: { CODEX_SANDBOX: 'seatbelt' }, fileExists: () => false });
assert.deepStrictEqual(result, { runtime: 'codex', source: 'session-env', signal: 'CODEX_SANDBOX' });
});
test('network-disabled sandbox var detects a codex session', () => {
const result = detectHostRuntime({
env: { CODEX_SANDBOX_NETWORK_DISABLED: '1' },
fileExists: () => false,
});
assert.deepStrictEqual(result, {
runtime: 'codex',
source: 'session-env',
signal: 'CODEX_SANDBOX_NETWORK_DISABLED',
});
});
test('CODEX_HOME with config.toml detects codex', () => {
const home = path.join(os.tmpdir(), 'fake-codex-home');
const spy = spyFileExists([path.join(home, 'config.toml')]);
const result = detectHostRuntime({ env: { CODEX_HOME: home }, fileExists: spy });
assert.deepStrictEqual(result, { runtime: 'codex', source: 'config-home', signal: 'CODEX_HOME' });
});
test('session signal outranks the config-home rung', () => {
const home = path.join(os.tmpdir(), 'fake-codex-home');
const spy = spyFileExists([path.join(home, 'config.toml')]);
const result = detectHostRuntime({
env: { CODEX_SANDBOX: 'seatbelt', CODEX_HOME: home },
fileExists: spy,
});
assert.strictEqual(result.source, 'session-env');
assert.deepStrictEqual(result, { runtime: 'codex', source: 'session-env', signal: 'CODEX_SANDBOX' });
});
test('no signal resolves to no detection', () => {
const result = detectHostRuntime({ env: {}, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
});
test('empty env value is not a signal', () => {
const result = detectHostRuntime({ env: { CODEX_SANDBOX: '' }, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
});
test('whitespace-only env value is not a signal', () => {
const result = detectHostRuntime({ env: { CODEX_SANDBOX: ' ' }, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
});
test('CODEX_HOME without config.toml is not a codex root', () => {
const home = path.join(os.tmpdir(), 'fake-codex-home-empty');
const spy = spyFileExists([]);
const result = detectHostRuntime({ env: { CODEX_HOME: home }, fileExists: spy });
assert.deepStrictEqual(result, NONE_RESULT);
assert.deepStrictEqual(spy.calls, [path.join(home, 'config.toml').replace(/\\/g, '/')]);
});
test('nonexistent CODEX_HOME degrades to no detection', () => {
const home = path.join(os.tmpdir(), 'msd-nonexistent-codex-home-' + Date.now());
const result = detectHostRuntime({ env: { CODEX_HOME: home }, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
});
test('CODEX_HOME pointing at a file degrades to no detection', (t) => {
const tmpFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'msd-codex-home-file-')), 'not-a-dir');
fs.writeFileSync(tmpFile, 'not a codex home\n');
t.after(() => cleanup(path.dirname(tmpFile)));
// No `fileExists` override: exercises the real fs.existsSync default.
assert.doesNotThrow(() => {
const result = detectHostRuntime({ env: { CODEX_HOME: tmpFile } });
assert.deepStrictEqual(result, NONE_RESULT);
});
});
test('empty CODEX_HOME is unset', () => {
const result = detectHostRuntime({ env: { CODEX_HOME: '' }, fileExists: () => true });
assert.deepStrictEqual(result, NONE_RESULT);
});
test('a throwing fileExists degrades rather than propagating', () => {
const throwingFileExists = () => {
const err = new Error('permission denied');
err.code = 'EACCES';
throw err;
};
assert.doesNotThrow(() => {
const result = detectHostRuntime({ env: { CODEX_HOME: '/some/codex/home' }, fileExists: throwingFileExists });
assert.deepStrictEqual(result, NONE_RESULT);
});
});
test('a throwing env degrades to the none-result rather than propagating', () => {
// The whole detection body — not just the fileExists probe — must be
// throw-safe: a proxied env whose GET trap throws must degrade like any
// other unreadable signal (#3245 fix 4).
const throwingEnv = new Proxy({}, { get() { throw new Error('boom'); } });
assert.doesNotThrow(() => {
const result = detectHostRuntime({ env: throwingEnv, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
});
});
test('the default codex home is never probed', () => {
// Every machine that has ever run Codex has ~/.codex/config.toml, so
// probing the DEFAULT home unconditionally would misreport a plain
// Claude session as codex just because Codex was installed at some
// point. Detection must require an explicit CODEX_HOME.
const defaultMarker = path.join(os.homedir(), '.codex', 'config.toml');
const spy = spyFileExists([defaultMarker]);
const result = detectHostRuntime({ env: {}, fileExists: spy });
assert.strictEqual(result.runtime, null);
assert.ok(
spy.calls.every((p) => !p.includes('/.codex/')),
`expected no probe of the default ~/.codex root, got: ${JSON.stringify(spy.calls)}`,
);
});
test('detection never writes shared defaults or any file (#2297)', (t) => {
const writeFileMock = mock.method(fs, 'writeFileSync', () => {});
const appendFileMock = mock.method(fs, 'appendFileSync', () => {});
const mkdirMock = mock.method(fs, 'mkdirSync', () => {});
t.after(() => {
writeFileMock.mock.restore();
appendFileMock.mock.restore();
mkdirMock.mock.restore();
});
detectHostRuntime({ env: {}, fileExists: () => false });
detectHostRuntime({ env: { CODEX_SANDBOX: 'seatbelt' }, fileExists: () => false });
detectHostRuntime({ env: { CODEX_HOME: '/some/codex/home' }, fileExists: () => true });
assert.strictEqual(writeFileMock.mock.callCount(), 0);
assert.strictEqual(appendFileMock.mock.callCount(), 0);
assert.strictEqual(mkdirMock.mock.callCount(), 0);
});
test('signal table is frozen and locked', () => {
assert.strictEqual(Object.isFrozen(CODEX_SESSION_ENV_SIGNALS), true);
assert.deepStrictEqual(
[...CODEX_SESSION_ENV_SIGNALS].sort(),
['CODEX_SANDBOX', 'CODEX_SANDBOX_NETWORK_DISABLED'],
);
});
});
// ─── resolveReportedRuntime precedence ──────────────────────────────────
describe('resolveReportedRuntime: precedence', () => {
function withProject(t) {
const savedMsdRuntime = process.env.MSD_RUNTIME;
delete process.env.MSD_RUNTIME;
const tmpDir = createTempProject();
t.after(() => {
cleanup(tmpDir);
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = savedMsdRuntime;
});
return tmpDir;
}
test('env runtime wins with no other signal', (t) => {
const tmpDir = withProject(t);
const result = resolveReportedRuntime(tmpDir, { env: { MSD_RUNTIME: 'opencode' } });
assert.strictEqual(result, 'opencode');
});
test('MSD_RUNTIME outranks config and detection', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, JSON.stringify({ runtime: 'claude' }));
const result = resolveReportedRuntime(tmpDir, {
env: { MSD_RUNTIME: 'opencode', CODEX_SANDBOX: 'seatbelt', CODEX_SANDBOX_NETWORK_DISABLED: '1' },
});
assert.strictEqual(result, 'opencode');
});
test('env alias normalizes before detection can fire', (t) => {
const tmpDir = withProject(t);
const result = resolveReportedRuntime(tmpDir, { env: { MSD_RUNTIME: 'codex-cli' } });
assert.strictEqual(result, 'codex');
});
test('invalid MSD_RUNTIME falls through to config', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, JSON.stringify({ runtime: 'cursor' }));
const result = resolveReportedRuntime(tmpDir, { env: { MSD_RUNTIME: ' ' } });
assert.strictEqual(result, 'cursor');
});
test('explicit config runtime outranks detection (#2517 preserved)', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, JSON.stringify({ runtime: 'claude' }));
const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
assert.strictEqual(result, 'claude');
});
test('explicit codex config unchanged', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, JSON.stringify({ runtime: 'codex' }));
const result = resolveReportedRuntime(tmpDir, { env: {} });
assert.strictEqual(result, 'codex');
});
test('no signal resolves to the claude default', (t) => {
const tmpDir = withProject(t);
const result = resolveReportedRuntime(tmpDir, { env: {} });
assert.strictEqual(result, 'claude');
});
test('missing config file still detects', (t) => {
const tmpDir = withProject(t);
const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
assert.strictEqual(result, 'codex');
});
test('malformed config json falls through to detection', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, '{ this is not json');
const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
assert.strictEqual(result, 'codex');
});
test('non-object config json is not a runtime source', (t) => {
const tmpDir = withProject(t);
const rawBodies = ['0', '"str"', '[]', 'null', 'true', ''];
for (const raw of rawBodies) {
writeConfig(tmpDir, raw);
assert.strictEqual(
resolveReportedRuntime(tmpDir, { env: {} }),
'claude',
`raw config body ${JSON.stringify(raw)} unexpectedly acted as a runtime source (no signal)`,
);
assert.strictEqual(
resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } }),
'codex',
`raw config body ${JSON.stringify(raw)} unexpectedly blocked detection`,
);
}
});
test('crlf config json is unaffected', (t) => {
const tmpDir = withProject(t);
const crlfConfig = ['{', ' "runtime": "cursor"', '}'].join('\r\n');
writeConfig(tmpDir, crlfConfig);
const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
assert.strictEqual(result, 'cursor');
});
// Deliberately NOT a "windows-shaped CODEX_HOME" test computed with
// path.join(...) on the same POSIX host as the production code: that
// construction passes by definition (both sides run the identical join)
// and cannot catch a separator bug. This test instead proves path.join is
// actually being used — a hand-rolled `home + '/' + marker` concatenation
// would double the separator when `home` already ends in one; path.join
// collapses it.
test('probe path is joined, not concatenated', (t) => {
const tmpDir = withProject(t);
const homeWithTrailingSep = path.join(os.tmpdir(), 'msd-3245-trailing') + path.sep;
const spy = spyFileExists([]);
const result = resolveReportedRuntime(tmpDir, {
env: { CODEX_HOME: homeWithTrailingSep },
fileExists: spy,
});
assert.strictEqual(result, 'claude');
assert.strictEqual(spy.calls.length, 1);
const probedPath = spy.calls[0];
assert.ok(
!probedPath.includes('//'),
`expected a normalized probe path with no doubled separator, got: ${probedPath}`,
);
assert.strictEqual(probedPath.split('/').pop(), CODEX_CONFIG_MARKER);
});
test('a throwing env degrades to the claude default rather than propagating', (t) => {
// resolveReportedRuntime's degraded answer is the 'claude' default, not
// the detection none-result — matching detectHostRuntime's contract one
// rung up the ladder (#3245 fix 4).
const tmpDir = withProject(t);
const throwingEnv = new Proxy({}, { get() { throw new Error('boom'); } });
assert.doesNotThrow(() => {
const result = resolveReportedRuntime(tmpDir, { env: throwingEnv, fileExists: () => false });
assert.strictEqual(result, 'claude');
});
});
test('detection never mutates the project config', (t) => {
const tmpDir = withProject(t);
writeConfig(tmpDir, JSON.stringify({ runtime: 'claude' }));
const configPath = path.join(tmpDir, '.planning', 'config.json');
const before = fs.readFileSync(configPath);
resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
const after = fs.readFileSync(configPath);
assert.deepStrictEqual(before, after);
});
test('default deps match injected deps', (t) => {
const tmpDir = withProject(t);
const savedCodexSandbox = process.env.CODEX_SANDBOX;
process.env.CODEX_SANDBOX = 'seatbelt';
t.after(() => {
if (savedCodexSandbox === undefined) delete process.env.CODEX_SANDBOX;
else process.env.CODEX_SANDBOX = savedCodexSandbox;
});
const viaDefaultDeps = resolveReportedRuntime(tmpDir);
const viaInjectedDeps = resolveReportedRuntime(tmpDir, { env: process.env });
assert.strictEqual(viaDefaultDeps, 'codex');
assert.strictEqual(viaDefaultDeps, viaInjectedDeps);
});
test('resolveReportedRuntime never writes shared defaults or any file (#2297)', (t) => {
// The #2297 negative proof above only wraps detectHostRuntime, but the
// function that actually ships is resolveReportedRuntime (called by
// withProjectRoot). This sibling wraps IT, across all three outcomes on
// its ladder: explicit-config hit, detection hit, and the plain 'claude'
// default.
//
// Fixture setup (createTempProject/writeConfig, which themselves call
// mkdirSync/writeFileSync) runs BEFORE the mocks are installed, so the
// fixtures land on real disk and only resolveReportedRuntime's own
// behavior is under observation.
const explicitDir = createTempProject();
writeConfig(explicitDir, JSON.stringify({ runtime: 'opencode' }));
const detectionDir = createTempProject();
const defaultDir = createTempProject();
// Each mock is restored via t.after() immediately after creation — not a
// try/finally in the test body (CONTRIBUTING.md bans that) — so restore
// still runs even if an earlier assertion below throws.
const writeFileMock = mock.method(fs, 'writeFileSync', () => {});
t.after(() => writeFileMock.mock.restore());
const appendFileMock = mock.method(fs, 'appendFileSync', () => {});
t.after(() => appendFileMock.mock.restore());
const mkdirMock = mock.method(fs, 'mkdirSync', () => {});
t.after(() => mkdirMock.mock.restore());
t.after(() => {
cleanup(explicitDir);
cleanup(detectionDir);
cleanup(defaultDir);
});
// Outcome 1: explicit config.json runtime wins.
assert.strictEqual(resolveReportedRuntime(explicitDir, { env: {} }), 'opencode');
// Outcome 2: no explicit config; host-detection rung fires.
assert.strictEqual(
resolveReportedRuntime(detectionDir, { env: { CODEX_SANDBOX: 'seatbelt' } }),
'codex',
);
// Outcome 3: no explicit config, no detection signal; plain default.
assert.strictEqual(resolveReportedRuntime(defaultDir, { env: {} }), 'claude');
assert.strictEqual(writeFileMock.mock.callCount(), 0);
assert.strictEqual(appendFileMock.mock.callCount(), 0);
assert.strictEqual(mkdirMock.mock.callCount(), 0);
});
});
// ─── runtime-slash frozen contract ──────────────────────────────────────
describe('runtime-slash: frozen contract (#3245 extraction must not move it)', () => {
test('resolveRuntime contract is unchanged by the extraction', (t) => {
const savedMsdRuntime = process.env.MSD_RUNTIME;
delete process.env.MSD_RUNTIME;
t.after(() => {
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = savedMsdRuntime;
});
assert.strictEqual(resolveRuntime(undefined), 'claude');
assert.strictEqual(resolveRuntime(null), 'claude');
assert.strictEqual(
resolveRuntime(path.join(os.tmpdir(), 'msd-nonexistent-project-' + Date.now())),
'claude',
);
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
assert.strictEqual(resolveRuntime(tmpDir), 'claude');
writeConfig(tmpDir, JSON.stringify({ runtime: 'opencode' }));
assert.strictEqual(resolveRuntime(tmpDir), 'opencode');
process.env.MSD_RUNTIME = 'cursor';
assert.strictEqual(resolveRuntime(tmpDir), 'cursor');
});
test('resolveRuntime ignores a codex session signal', (t) => {
// Detection is deliberately NOT wired into resolveRuntime — only
// resolveReportedRuntime's separate ladder consults it — so command-style
// emission (formatMsdSlash et al.) does not move (ADR-2313 scope boundary).
const savedMsdRuntime = process.env.MSD_RUNTIME;
const savedCodexSandbox = process.env.CODEX_SANDBOX;
delete process.env.MSD_RUNTIME;
process.env.CODEX_SANDBOX = 'seatbelt';
t.after(() => {
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = savedMsdRuntime;
if (savedCodexSandbox === undefined) delete process.env.CODEX_SANDBOX;
else process.env.CODEX_SANDBOX = savedCodexSandbox;
});
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
assert.strictEqual(resolveRuntime(tmpDir), 'claude');
});
test('explicit claude is distinguishable from the default', (t) => {
const savedMsdRuntime = process.env.MSD_RUNTIME;
delete process.env.MSD_RUNTIME;
t.after(() => {
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = savedMsdRuntime;
});
const tmpDir = createTempProject();
t.after(() => cleanup(tmpDir));
assert.strictEqual(resolveExplicitRuntime(tmpDir), null);
writeConfig(tmpDir, JSON.stringify({ runtime: 'claude' }));
assert.strictEqual(resolveExplicitRuntime(tmpDir), 'claude');
});
});
// ─── Generative-fix-divergence parity with inferPreferredRuntime ────────
describe('parity with update-context.cjs:inferPreferredRuntime (generative-fix-divergence guard)', () => {
// Two surfaces independently encode "what indicates Codex" —
// inferPreferredRuntime (update.md's version-detection ladder) and
// detectHostRuntime (init's agent_runtime rung). This pins them to agree
// on the CODEX_HOME signal so a future edit to one cannot silently diverge
// from the other.
test('codex marker filename is single-sourced', () => {
// The ONLY thing actually shared between the two functions: the marker
// FILENAME. update-context.cjs is the owner; host-runtime-detection.cjs
// imports and re-exports it. This asserts the re-export is the same
// binding (strict equality), not merely an independently-matching literal.
assert.strictEqual(CODEX_CONFIG_MARKER, 'config.toml');
assert.strictEqual(CODEX_CONFIG_MARKER, updateContext.CODEX_CONFIG_MARKER);
});
test('detection requires the marker where inferPreferredRuntime does not', () => {
// THE DIVERGENCE POINT (deliberate, not a bug — see the header comment on
// CODEX_CONFIG_HOME_ENV in src/host-runtime-detection.cts and the "Host
// Runtime Detection Module" entry in CONTEXT.md). With CODEX_HOME set and
// NO config.toml present:
// - inferPreferredRuntime resolves an UPDATE CONTEXT, where a bare
// CODEX_HOME is a sufficient hint -> 'codex'.
// - detectHostRuntime asserts SESSION IDENTITY and requires the
// stronger marker-file signal -> no detection (null).
// This test exists so that difference is a recorded decision rather than
// an accident: it fails loudly if either side's rule ever changes.
const dir = path.join(os.tmpdir(), 'not-a-real-codex-home');
const env = { CODEX_HOME: dir };
const inferred = updateContext.inferPreferredRuntime({
fs: { exists: () => false, readFile: () => null },
env,
preferredConfigDir: '',
});
assert.strictEqual(inferred, 'codex');
const detected = detectHostRuntime({ env, fileExists: () => false });
assert.strictEqual(detected.runtime, null);
});
test('config.toml is the shared codex marker filename', () => {
const dir = path.join(os.tmpdir(), 'preferred-config-dir');
const marker = path.join(dir, CODEX_CONFIG_MARKER);
const inferred = updateContext.inferPreferredRuntime({
fs: { exists: (p) => p === marker, readFile: () => null },
env: {},
preferredConfigDir: dir,
});
assert.strictEqual(inferred, 'codex');
assert.strictEqual(CODEX_CONFIG_MARKER, 'config.toml');
});
});
// ─── Properties ──────────────────────────────────────────────────────────
describe('detectHostRuntime: properties', () => {
const FC_OPTS = { numRuns: 200, seed: 32452843, verbose: true };
test('property — no signal key implies no detection', () => {
const excludedKeys = new Set([...CODEX_SESSION_ENV_SIGNALS, CODEX_CONFIG_HOME_ENV]);
fc.assert(
fc.property(
fc.dictionary(
fc.string({ minLength: 1, maxLength: 12 }).filter((k) => !excludedKeys.has(k)),
fc.string(),
),
(env) => {
const result = detectHostRuntime({ env, fileExists: () => false });
assert.deepStrictEqual(result, NONE_RESULT);
},
),
FC_OPTS,
);
});
test('property — any non-empty signal value detects codex', () => {
fc.assert(
fc.property(
fc.constantFrom(...CODEX_SESSION_ENV_SIGNALS),
fc.string().filter((s) => s.trim() !== ''),
(key, value) => {
const result = detectHostRuntime({ env: { [key]: value }, fileExists: () => false });
assert.deepStrictEqual(result, { runtime: 'codex', source: 'session-env', signal: key });
},
),
FC_OPTS,
);
});
});
// ── #4717 — the per-install .msd-runtime marker outranks host detection ──────
// On a multi-runtime machine (CODEX_HOME exported globally), host sniffing
// misreports every Claude Code session as codex. The per-install marker names
// the runtime that owns THIS tree — a stronger signal than host detection.
// Ladder: explicit (MSD_RUNTIME / config.runtime) > marker > host detection >
// 'claude'. Marker-less trees (dev/source, pre-#2297 installs) are unchanged.
describe('resolveReportedRuntime: install-marker rung (#4717)', () => {
const slash = require('../msd-core/bin/lib/runtime-slash.cjs');
// Self-contained withProject: clears MSD_RUNTIME (the marker rung sits below
// it) and scaffolds a throwaway project — scoped to this describe so the
// sibling precedence describe's own helper is untouched.
function withProject4717(t) {
const savedMsdRuntime = process.env.MSD_RUNTIME;
delete process.env.MSD_RUNTIME;
const tmpDir = createTempProject();
t.after(() => {
cleanup(tmpDir);
if (savedMsdRuntime === undefined) delete process.env.MSD_RUNTIME;
else process.env.MSD_RUNTIME = savedMsdRuntime;
});
return tmpDir;
}
function writeConfig4717(tmpDir, runtime) {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ runtime }),
);
}
test('install marker outranks host detection (claude session on a codex-sniffing machine)', (t) => {
const tmpDir = withProject4717(t);
slash._setInstallRuntimeMarkerForTests('claude');
t.after(() => slash._resetInstallRuntimeMarkerCacheForTests());
// CODEX_SANDBOX makes host detection report codex — the issue's repro shape.
const result = resolveReportedRuntime(tmpDir, {
env: { CODEX_SANDBOX: 'seatbelt', CODEX_SANDBOX_NETWORK_DISABLED: '1' },
});
assert.strictEqual(result, 'claude',
'the marker (claude) must outrank host detection (codex) on the owning tree');
});
test('install marker supplies the runtime when host detection finds nothing (codex install)', (t) => {
const tmpDir = withProject4717(t);
slash._setInstallRuntimeMarkerForTests('codex');
t.after(() => slash._resetInstallRuntimeMarkerCacheForTests());
const result = resolveReportedRuntime(tmpDir, { env: {} });
assert.strictEqual(result, 'codex',
'a codex-owned tree reports codex even where host sniffing would say claude');
});
test('explicit MSD_RUNTIME still outranks the marker (#4717 ladder rung 1)', (t) => {
const tmpDir = withProject4717(t);
slash._setInstallRuntimeMarkerForTests('codex');
t.after(() => slash._resetInstallRuntimeMarkerCacheForTests());
const result = resolveReportedRuntime(tmpDir, { env: { MSD_RUNTIME: 'claude' } });
assert.strictEqual(result, 'claude');
});
test('explicit config runtime still outranks the marker (#4717 ladder rung 1)', (t) => {
const tmpDir = withProject4717(t);
writeConfig4717(tmpDir, 'cursor');
slash._setInstallRuntimeMarkerForTests('codex');
t.after(() => slash._resetInstallRuntimeMarkerCacheForTests());
const result = resolveReportedRuntime(tmpDir, { env: {} });
assert.strictEqual(result, 'cursor');
});
test('no marker: host detection unchanged (dev/source trees, pre-#2297 installs)', (t) => {
const tmpDir = withProject4717(t);
slash._resetInstallRuntimeMarkerCacheForTests();
const result = resolveReportedRuntime(tmpDir, { env: { CODEX_SANDBOX: 'seatbelt' } });
assert.strictEqual(result, 'codex', 'host detection remains the fallback without a marker');
});
});