made extension tmux-only, replaced cmux.ts with tmux.ts (~1368 -> 354 lines)

Removed the cmux, zellij, and wezterm backends, the MuxBackend type, the
PI_SUBAGENT_MUX override, and dead exports (renameCurrentTab, renameWorkspace,
sendEscape, isFishShell, exitStatusVar). The ten-function surface API used by
index.ts is unchanged. Integration harness and tests are tmux-only; README and
package.json now state tmux-only with a fork note pointing multi-multiplexer
users at the upstream project.
This commit is contained in:
Amos
2026-08-20 18:48:14 +03:00
parent 865648c4f1
commit 9cbc5b4878
9 changed files with 421 additions and 1848 deletions
+27 -92
View File
@@ -2,11 +2,11 @@
* Integration test harness for pi-interactive-subagents.
*
* Provides utilities to:
* - Detect available mux backends (cmux, tmux, zellij)
* - Detect whether tmux is available
* - Create isolated test environments with test agent definitions
* - Start real pi sessions in mux surfaces
* - Start real pi sessions in tmux panes
* - Poll for file creation and screen output
* - Clean up surfaces and temp files after tests
* - Clean up panes and temp files after tests
*/
import { execFileSync } from "node:child_process";
import {
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import {
getMuxBackend,
isMuxAvailable,
createSurface,
createSurfaceSplit,
sendCommand,
@@ -31,14 +31,10 @@ import {
readScreen,
readScreenAsync,
closeSurface,
sendEscape,
shellEscape,
parseCmuxFocusedSnapshotFromJson,
parseCmuxPaneRefForSurfaceFromJson,
type MuxBackend,
} from "../../pi-extension/subagents/cmux.ts";
} from "../../pi-extension/subagents/tmux.ts";
// Re-export mux primitives for tests
// Re-export tmux primitives for tests
export {
createSurface,
createSurfaceSplit,
@@ -47,10 +43,8 @@ export {
readScreen,
readScreenAsync,
closeSurface,
sendEscape,
shellEscape,
};
export type { MuxBackend };
// ── Paths ──
@@ -82,99 +76,42 @@ export const PI_TIMEOUT = Number(process.env.PI_TEST_TIMEOUT ?? "120000");
// ── Backend detection ──
/**
* Detect which mux backends are actually available in the current environment.
* Temporarily sets PI_SUBAGENT_MUX to probe each backend.
* Detect whether tmux is available in the current environment.
* Returns ["tmux"] or [].
*/
export function getAvailableBackends(): MuxBackend[] {
const backends: MuxBackend[] = [];
const orig = process.env.PI_SUBAGENT_MUX;
for (const backend of ["cmux", "tmux", "zellij"] as MuxBackend[]) {
process.env.PI_SUBAGENT_MUX = backend;
try {
if (getMuxBackend() === backend) backends.push(backend);
} catch {}
}
if (orig === undefined) delete process.env.PI_SUBAGENT_MUX;
else process.env.PI_SUBAGENT_MUX = orig;
return backends;
export function getAvailableBackends(): string[] {
return isMuxAvailable() ? ["tmux"] : [];
}
export function setBackend(backend: MuxBackend): string | undefined {
const prev = process.env.PI_SUBAGENT_MUX;
process.env.PI_SUBAGENT_MUX = backend;
return prev;
export function focusSurface(surface: string): void {
execFileSync("tmux", ["select-pane", "-t", surface], { encoding: "utf8" });
}
export function restoreBackend(prev: string | undefined): void {
if (prev === undefined) delete process.env.PI_SUBAGENT_MUX;
else process.env.PI_SUBAGENT_MUX = prev;
}
export function focusSurface(backend: MuxBackend, surface: string): void {
if (backend === "cmux") {
const pane = getSurfacePane(backend, surface);
if (pane) execFileSync("cmux", ["focus-pane", "--pane", pane], { encoding: "utf8" });
execFileSync("cmux", ["focus-panel", "--panel", surface], { encoding: "utf8" });
return;
export function getFocusedSurface(): string | null {
try {
const panes = execFileSync("tmux", ["list-panes", "-F", "#{pane_id} #{pane_active}"], {
encoding: "utf8",
});
const activeLine = panes.split("\n").find((line) => line.endsWith(" 1"));
return activeLine?.split(" ")[0] ?? null;
} catch {
return null;
}
if (backend === "tmux") {
execFileSync("tmux", ["select-pane", "-t", surface], { encoding: "utf8" });
return;
}
throw new Error(`Focus helpers are not implemented for ${backend}`);
}
export function getFocusedSurface(backend: MuxBackend): string | null {
if (backend === "cmux") {
const info = execFileSync("cmux", ["identify", "--json"], { encoding: "utf8" });
return parseCmuxFocusedSnapshotFromJson(info)?.surfaceRef ?? null;
}
if (backend === "tmux") {
try {
const panes = execFileSync("tmux", ["list-panes", "-F", "#{pane_id} #{pane_active}"], {
encoding: "utf8",
});
const activeLine = panes.split("\n").find((line) => line.endsWith(" 1"));
return activeLine?.split(" ")[0] ?? null;
} catch {
return null;
}
}
throw new Error(`Focus helpers are not implemented for ${backend}`);
}
export function getSurfacePane(backend: MuxBackend, surface: string): string | null {
if (backend === "cmux") {
const info = execFileSync("cmux", ["identify", "--surface", surface], { encoding: "utf8" });
return parseCmuxPaneRefForSurfaceFromJson(info, surface);
}
if (backend === "tmux") return surface;
throw new Error(`Pane lookup is not implemented for ${backend}`);
}
export async function waitForFocusedSurface(
backend: MuxBackend,
surface: string,
timeout: number = PI_TIMEOUT,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
if (getFocusedSurface(backend) === surface) return;
if (getFocusedSurface() === surface) return;
await sleep(200);
}
throw new Error(
`Timeout (${timeout}ms) waiting for focused ${backend} surface ${surface}; ` +
`current focus is ${getFocusedSurface(backend) ?? "unknown"}`,
`Timeout (${timeout}ms) waiting for focused tmux pane ${surface}; ` +
`current focus is ${getFocusedSurface() ?? "unknown"}`,
);
}
@@ -183,9 +120,7 @@ export async function waitForFocusedSurface(
export interface TestEnv {
/** Temp directory serving as the test project root */
dir: string;
/** Active mux backend for this test run */
backend: MuxBackend;
/** Surfaces created during the test (cleaned up automatically) */
/** Panes created during the test (cleaned up automatically) */
surfaces: string[];
/** Temp files to clean up */
tempFiles: string[];
@@ -195,7 +130,7 @@ export interface TestEnv {
* Create an isolated test environment with test agent definitions.
* The temp dir has `.pi/agents/` containing copies of all test agents.
*/
export function createTestEnv(backend: MuxBackend): TestEnv {
export function createTestEnv(): TestEnv {
const dir = mkdtempSync(join(tmpdir(), "pi-integ-"));
const agentsDir = join(dir, ".pi", "agents");
mkdirSync(agentsDir, { recursive: true });
@@ -209,7 +144,7 @@ export function createTestEnv(backend: MuxBackend): TestEnv {
}
}
return { dir, backend, surfaces: [], tempFiles: [] };
return { dir, surfaces: [], tempFiles: [] };
}
/**
+5 -11
View File
@@ -2,14 +2,13 @@
* Integration tests for the full subagent lifecycle.
*
* These tests spawn REAL pi sessions with REAL LLM calls (haiku by default).
* Each test creates a mux surface, runs pi with a task that uses the subagent
* Each test creates a tmux pane, runs pi with a task that uses the subagent
* tool, and verifies the outcome via marker files and screen output.
*
* Costs: ~$0.01-0.05 per test run (haiku).
* Duration: ~30-90s per test.
*
* Run inside a supported multiplexer:
* cmux bash -c 'npm run test:integration'
* Run inside tmux:
* tmux new 'npm run test:integration'
*
* Configuration:
@@ -21,8 +20,6 @@ import assert from "node:assert/strict";
import { existsSync, readFileSync, unlinkSync } from "node:fs";
import {
getAvailableBackends,
setBackend,
restoreBackend,
createTestEnv,
cleanupTestEnv,
createTrackedSurface,
@@ -40,23 +37,20 @@ import {
const backends = getAvailableBackends();
if (backends.length === 0) {
console.log("⚠️ No mux backend available — skipping subagent lifecycle integration tests");
console.log(" Run inside cmux or tmux to enable these tests.");
console.log("⚠️ tmux is not available — skipping subagent lifecycle integration tests");
console.log(" Run inside tmux to enable these tests.");
}
for (const backend of backends) {
describe(`subagent-lifecycle [${backend}]`, { timeout: PI_TIMEOUT * 3 }, () => {
let prevMux: string | undefined;
let env: TestEnv;
before(() => {
prevMux = setBackend(backend);
env = createTestEnv(backend);
env = createTestEnv();
});
after(() => {
cleanupTestEnv(env);
restoreBackend(prevMux);
});
// ── Basic spawn + completion ──
@@ -1,29 +1,24 @@
/**
* Integration tests for the multiplexer surface layer.
* Integration tests for the tmux surface layer.
*
* These tests exercise real mux operations: creating panes,
* sending commands, reading screen output, and closing surfaces.
* These tests exercise real tmux operations: creating panes,
* sending commands, reading screen output, and closing panes.
* No LLM calls — fast and free.
*
* Run inside a supported multiplexer:
* cmux bash -c 'npm run test:integration'
* Run inside tmux:
* tmux new 'npm run test:integration'
* zellij --session pi # then run: npm run test:integration
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { unlinkSync } from "node:fs";
import {
getAvailableBackends,
setBackend,
restoreBackend,
createTestEnv,
cleanupTestEnv,
createTrackedSurface,
createTrackedSurfaceSplit,
focusSurface,
getFocusedSurface,
getSurfacePane,
waitForFocusedSurface,
untrackSurface,
sendCommand,
@@ -31,7 +26,6 @@ import {
readScreen,
readScreenAsync,
closeSurface,
sendEscape,
sleep,
uniqueId,
trackTempFile,
@@ -44,47 +38,36 @@ const backends = getAvailableBackends();
const FOCUS_TEST_SHELL_READY_DELAY_MS = Number(process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS ?? "2500");
if (backends.length === 0) {
console.log("⚠️ No mux backend available — skipping mux-surface integration tests");
console.log(" Run inside cmux or tmux to enable these tests.");
console.log("⚠️ tmux is not available — skipping tmux-surface integration tests");
console.log(" Run inside tmux to enable these tests.");
}
for (const backend of backends) {
describe(`mux-surface [${backend}]`, { timeout: 60_000 }, () => {
let prevMux: string | undefined;
describe(`tmux-surface [${backend}]`, { timeout: 60_000 }, () => {
let env: TestEnv;
before(() => {
prevMux = setBackend(backend);
env = createTestEnv(backend);
env = createTestEnv();
});
after(() => {
cleanupTestEnv(env);
restoreBackend(prevMux);
});
it("keeps focus on the active surface while creating and targeting subagent surfaces", async () => {
const anchor = createTrackedSurfaceSplit(env, "focus-anchor", "right");
await sleep(1000);
focusSurface(backend, anchor);
await waitForFocusedSurface(backend, anchor, 10_000);
focusSurface(anchor);
await waitForFocusedSurface(anchor, 10_000);
const childA = createTrackedSurface(env, "focus-child-a");
await sleep(FOCUS_TEST_SHELL_READY_DELAY_MS);
assert.equal(getFocusedSurface(backend), anchor);
assert.equal(getFocusedSurface(), anchor);
const childB = createTrackedSurface(env, "focus-child-b");
await sleep(FOCUS_TEST_SHELL_READY_DELAY_MS);
assert.equal(getFocusedSurface(backend), anchor);
if (backend === "cmux") {
const paneA = getSurfacePane(backend, childA);
const paneB = getSurfacePane(backend, childB);
assert.ok(paneA, `Expected pane ref for ${childA}`);
assert.ok(paneB, `Expected pane ref for ${childB}`);
assert.equal(paneB, paneA);
}
assert.equal(getFocusedSurface(), anchor);
const markerA = uniqueId();
const markerB = uniqueId();
@@ -95,7 +78,7 @@ for (const backend of backends) {
waitForScreen(childA, new RegExp(`FOCUS_A_${markerA}`), 20_000, 50),
waitForScreen(childB, new RegExp(`FOCUS_B_${markerB}`), 20_000, 50),
]);
assert.equal(getFocusedSurface(backend), anchor);
assert.equal(getFocusedSurface(), anchor);
});
it("creates a surface, sends a command, reads output, and closes it", async () => {
@@ -197,7 +180,7 @@ for (const backend of backends) {
await sleep(1000);
const marker = uniqueId();
const filePath = `/tmp/pi-mux-test-${marker}.txt`;
const filePath = `/tmp/pi-tmux-test-${marker}.txt`;
sendCommand(surface, `echo "FILE_${marker}" > ${filePath} && echo "WRITTEN_${marker}"`);
@@ -210,34 +193,5 @@ for (const backend of backends) {
unlinkSync(filePath);
} catch {}
});
it("delivers Escape as byte 27 to the target surface", async () => {
const surface = createTrackedSurface(env, "escape-byte-test");
await sleep(1000);
const marker = uniqueId();
const byteFile = `/tmp/pi-mux-escape-${marker}.txt`;
trackTempFile(env, byteFile);
const nodeProgram =
"const fs = require('node:fs');" +
"if (!process.stdin.isTTY) throw new Error('stdin is not a TTY');" +
"process.stdin.setRawMode(true);" +
"process.stdin.resume();" +
"process.stdout.write('ESC_READY\\n');" +
"process.stdin.once('data', (chunk) => {" +
`fs.writeFileSync(${JSON.stringify(byteFile)}, Array.from(chunk).join(','));` +
"process.exit(0);" +
"});";
const command = `node -e ${JSON.stringify(nodeProgram)}`;
sendLongCommand(surface, command);
await waitForScreen(surface, /ESC_READY/, 15_000, 50);
sendEscape(surface);
const content = await waitForFile(byteFile, 15_000, /^27$/);
assert.equal(content.trim(), "27");
});
});
}
+4 -284
View File
@@ -30,20 +30,7 @@ import {
summarizeSessionStats,
} from "../pi-extension/subagents/session.ts";
import {
shellEscape,
isCmuxAvailable,
isWezTermAvailable,
parseCmuxFocusedSnapshot,
parseCmuxFocusedSnapshotFromJson,
parseCmuxJson,
parseCmuxPaneRefForSurface,
parseCmuxPaneRefForSurfaceFromJson,
canSplitZellijPane,
predictZellijSplitDirection,
selectZellijPlacement,
selectZellijStackPlacement,
} from "../pi-extension/subagents/cmux.ts";
import { shellEscape } from "../pi-extension/subagents/tmux.ts";
import {
advanceStatusState,
capStatusLines,
@@ -69,7 +56,7 @@ import {
runningChildrenCount,
} from "../pi-extension/subagents/subagent-done.ts";
import subagentDoneExtension from "../pi-extension/subagents/subagent-done.ts";
import { __pollForExitTest__ } from "../pi-extension/subagents/cmux.ts";
import { __pollForExitTest__ } from "../pi-extension/subagents/tmux.ts";
// --- Helpers ---
@@ -1754,7 +1741,7 @@ describe("subagent-done.ts", () => {
});
});
describe("cmux.ts interpretExitSidecar", () => {
describe("tmux.ts interpretExitSidecar", () => {
const { interpretExitSidecar } = __pollForExitTest__;
it("no longer decodes ping payloads (ask_question keeps the session open instead)", () => {
@@ -2665,7 +2652,7 @@ describe("subagent display helpers", () => {
});
});
describe("cmux.ts", () => {
describe("tmux.ts", () => {
describe("shellEscape", () => {
it("wraps in single quotes", () => {
assert.equal(shellEscape("hello"), "'hello'");
@@ -2688,271 +2675,4 @@ describe("cmux.ts", () => {
assert.ok(escaped.includes("$world"));
});
});
describe("parseCmuxFocusedSnapshot", () => {
it("parses focused surface and pane refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshot({ focused: { surface_ref: "surface:3", pane_ref: "pane:2" } }),
{ surfaceRef: "surface:3", paneRef: "pane:2" },
);
});
it("does not fall back to caller refs", () => {
assert.equal(
parseCmuxFocusedSnapshot({ caller: { surface_ref: "surface:1", pane_ref: "pane:1" } }),
null,
);
});
it("returns null for malformed values", () => {
assert.equal(parseCmuxFocusedSnapshot(null), null);
assert.equal(parseCmuxFocusedSnapshot({ focused: {} }), null);
});
});
describe("parseCmuxJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxJson("not json"), null);
});
it("parses valid JSON text", () => {
assert.deepEqual(parseCmuxJson('{"ok":true}'), { ok: true });
});
});
describe("parseCmuxFocusedSnapshotFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxFocusedSnapshotFromJson("not json"), null);
});
it("returns null when focused is absent or not an object", () => {
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"focused":null,"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
assert.equal(
parseCmuxFocusedSnapshotFromJson('{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"}}'),
null,
);
});
it("parses focused refs without falling back to caller refs", () => {
assert.deepEqual(
parseCmuxFocusedSnapshotFromJson(
'{"caller":{"surface_ref":"surface:1","pane_ref":"pane:1"},"focused":{"surface_ref":"surface:2","pane_ref":"pane:3"}}',
),
{ surfaceRef: "surface:2", paneRef: "pane:3" },
);
});
});
describe("parseCmuxPaneRefForSurface", () => {
it("parses top-level pane refs for a surface", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:7", pane_ref: "pane:4" }, "surface:7"),
"pane:4",
);
});
it("parses caller pane refs for identify --surface output", () => {
assert.equal(
parseCmuxPaneRefForSurface(
{ caller: { surface_ref: "surface:7", pane_ref: "pane:4" } },
"surface:7",
),
"pane:4",
);
});
it("returns null when the surface does not match", () => {
assert.equal(
parseCmuxPaneRefForSurface({ surface_ref: "surface:8", pane_ref: "pane:4" }, "surface:7"),
null,
);
});
});
describe("parseCmuxPaneRefForSurfaceFromJson", () => {
it("returns null for malformed JSON text", () => {
assert.equal(parseCmuxPaneRefForSurfaceFromJson("not json", "surface:7"), null);
});
it("parses caller refs from cmux identify --surface JSON text", () => {
assert.equal(
parseCmuxPaneRefForSurfaceFromJson(
'{"caller":{"surface_ref":"surface:7","pane_ref":"pane:4"}}',
"surface:7",
),
"pane:4",
);
});
});
describe("zellij placement", () => {
const pane = (overrides: any) => ({
id: 1,
is_plugin: false,
is_floating: false,
is_selectable: true,
exited: false,
pane_rows: 20,
pane_columns: 80,
tab_id: 1,
...overrides,
});
it("matches Zellij direction and minimum split rules", () => {
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 11 })), "right");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 11, pane_columns: 5 })), "down");
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 5, pane_columns: 10 })), null);
assert.equal(predictZellijSplitDirection(pane({ pane_rows: 4, pane_columns: 80 })), null);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 11 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 11, pane_columns: 5 })), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 5, pane_columns: 10 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 4, pane_columns: 80 })), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 100 }), 80, 20), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 45, pane_columns: 100 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 30, pane_columns: 170 }), 80, 20), true);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 47 }), 50, 10), false);
assert.equal(canSplitZellijPane(pane({ pane_rows: 31, pane_columns: 77 }), 50, 10), true);
});
it("uses tab-scoped split only when all Zellij split candidates are safe", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 40, pane_columns: 120 }),
pane({ id: 11, tab_id: 1, pane_rows: 120, pane_columns: 100 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "split",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
splitDirection: "down",
});
});
it("stacks when any Zellij split candidate would fall below Pi's configured minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 100, pane_columns: 47 }),
pane({ id: 11, tab_id: 1, pane_rows: 31, pane_columns: 77 }),
],
10,
50,
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("stacks when Zellij would split a pane below Pi's usable minimum", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 20, pane_columns: 20 }),
pane({ id: 11, tab_id: 1, pane_rows: 18, pane_columns: 60 }),
pane({ id: 12, tab_id: 1, pane_rows: 10, pane_columns: 70 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("never chooses the parent pane as the stack target", () => {
const plan = selectZellijStackPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 }),
pane({ id: 11, tab_id: 1, pane_rows: 10, pane_columns: 20 }),
pane({ id: 12, tab_id: 1, pane_rows: 8, pane_columns: 30 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 12,
targetPaneId: 12,
tabId: 1,
});
});
it("does not stack when the only usable pane is the parent", () => {
const plan = selectZellijStackPlacement(
[pane({ id: 10, tab_id: 1, pane_rows: 60, pane_columns: 200 })],
10,
);
assert.equal(plan, null);
});
it("stacks on the largest usable non-parent pane when none can split", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 6, pane_columns: 8 }),
pane({ id: 12, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.deepEqual(plan, {
mode: "stack",
anchorPaneId: 11,
targetPaneId: 11,
tabId: 1,
});
});
it("ignores floating, plugin, exited, unselectable, and other-tab panes", () => {
const plan = selectZellijPlacement(
[
pane({ id: 10, tab_id: 1, pane_rows: 5, pane_columns: 10 }),
pane({ id: 11, tab_id: 1, pane_rows: 60, pane_columns: 200, is_floating: true }),
pane({ id: 12, tab_id: 1, pane_rows: 60, pane_columns: 200, is_plugin: true }),
pane({ id: 13, tab_id: 1, pane_rows: 60, pane_columns: 200, exited: true }),
pane({ id: 14, tab_id: 1, pane_rows: 60, pane_columns: 200, is_selectable: false }),
pane({ id: 15, tab_id: 2, pane_rows: 60, pane_columns: 200 }),
],
10,
);
assert.equal(plan, null);
});
it("returns null when the parent pane cannot be found", () => {
assert.equal(selectZellijPlacement([pane({ id: 10 })], 99), null);
});
});
describe("isCmuxAvailable", () => {
it("returns boolean based on CMUX_SOCKET_PATH", () => {
// Can't easily mock env in node:test, just verify it returns a boolean
const result = isCmuxAvailable();
assert.equal(typeof result, "boolean");
});
});
describe("isWezTermAvailable", () => {
it("returns boolean based on WEZTERM_UNIX_SOCKET", () => {
const result = isWezTermAvailable();
assert.equal(typeof result, "boolean");
});
});
});