Initial commit
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Integration tests for the multiplexer surface layer.
|
||||
*
|
||||
* These tests exercise real mux operations: creating panes,
|
||||
* sending commands, reading screen output, and closing surfaces.
|
||||
* No LLM calls — fast and free.
|
||||
*
|
||||
* Run inside a supported multiplexer:
|
||||
* cmux bash -c 'npm run test:integration'
|
||||
* 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,
|
||||
sendLongCommand,
|
||||
readScreen,
|
||||
readScreenAsync,
|
||||
closeSurface,
|
||||
sendEscape,
|
||||
sleep,
|
||||
uniqueId,
|
||||
trackTempFile,
|
||||
waitForFile,
|
||||
waitForScreen,
|
||||
type TestEnv,
|
||||
} from "./harness.ts";
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
for (const backend of backends) {
|
||||
describe(`mux-surface [${backend}]`, { timeout: 60_000 }, () => {
|
||||
let prevMux: string | undefined;
|
||||
let env: TestEnv;
|
||||
|
||||
before(() => {
|
||||
prevMux = setBackend(backend);
|
||||
env = createTestEnv(backend);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const childA = createTrackedSurface(env, "focus-child-a");
|
||||
await sleep(FOCUS_TEST_SHELL_READY_DELAY_MS);
|
||||
assert.equal(getFocusedSurface(backend), 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);
|
||||
}
|
||||
|
||||
const markerA = uniqueId();
|
||||
const markerB = uniqueId();
|
||||
sendCommand(childA, `echo "FOCUS_A_${markerA}"`);
|
||||
sendCommand(childB, `echo "FOCUS_B_${markerB}"`);
|
||||
|
||||
await Promise.all([
|
||||
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);
|
||||
});
|
||||
|
||||
it("creates a surface, sends a command, reads output, and closes it", async () => {
|
||||
const surface = createTrackedSurface(env, "echo-test");
|
||||
await sleep(1000);
|
||||
|
||||
const marker = uniqueId();
|
||||
sendCommand(surface, `echo "MARKER_${marker}"`);
|
||||
await sleep(1500);
|
||||
|
||||
const screen = readScreen(surface, 50);
|
||||
assert.ok(
|
||||
screen.includes(`MARKER_${marker}`),
|
||||
`Expected screen to contain MARKER_${marker}. Got:\n${screen}`,
|
||||
);
|
||||
|
||||
closeSurface(surface);
|
||||
untrackSurface(env, surface);
|
||||
});
|
||||
|
||||
it("preserves shell special characters in echo output", async () => {
|
||||
const surface = createTrackedSurface(env, "escape-test");
|
||||
await sleep(1000);
|
||||
|
||||
const marker = uniqueId();
|
||||
// Single-quoted string — $ and " are literal inside single quotes
|
||||
sendCommand(surface, `echo 'SPEC_${marker}_$HOME_"quotes"_done'`);
|
||||
await sleep(1500);
|
||||
|
||||
const screen = readScreen(surface, 50);
|
||||
assert.ok(
|
||||
screen.includes(`SPEC_${marker}`),
|
||||
`Expected special-char output. Got:\n${screen}`,
|
||||
);
|
||||
// $ should be literal inside single quotes
|
||||
assert.ok(
|
||||
screen.includes("$HOME"),
|
||||
`Expected literal $HOME in output. Got:\n${screen}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a long command via script file without truncation", async () => {
|
||||
const surface = createTrackedSurface(env, "long-cmd-test");
|
||||
await sleep(1000);
|
||||
|
||||
const marker = uniqueId();
|
||||
const longValue = "X".repeat(500);
|
||||
const command = `echo "LONG_${marker}_${longValue}_END"`;
|
||||
|
||||
sendLongCommand(surface, command);
|
||||
await sleep(2000);
|
||||
|
||||
const screen = readScreen(surface, 50);
|
||||
assert.ok(
|
||||
screen.includes(`LONG_${marker}`),
|
||||
`Expected long command output. Got:\n${screen.slice(0, 300)}...`,
|
||||
);
|
||||
assert.ok(
|
||||
screen.includes("_END"),
|
||||
`Expected full output (not truncated). Got:\n${screen.slice(-300)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("reads screen asynchronously", async () => {
|
||||
const surface = createTrackedSurface(env, "async-read-test");
|
||||
await sleep(1000);
|
||||
|
||||
const marker = uniqueId();
|
||||
sendCommand(surface, `echo "ASYNC_${marker}"`);
|
||||
await sleep(1500);
|
||||
|
||||
const screen = await readScreenAsync(surface, 50);
|
||||
assert.ok(
|
||||
screen.includes(`ASYNC_${marker}`),
|
||||
`Async read should find marker. Got:\n${screen}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("manages multiple surfaces concurrently", async () => {
|
||||
const s1 = createTrackedSurface(env, "multi-1");
|
||||
const s2 = createTrackedSurface(env, "multi-2");
|
||||
await sleep(1500);
|
||||
|
||||
const m1 = uniqueId();
|
||||
const m2 = uniqueId();
|
||||
sendCommand(s1, `echo "S1_${m1}"`);
|
||||
sendCommand(s2, `echo "S2_${m2}"`);
|
||||
await sleep(1500);
|
||||
|
||||
const screen1 = readScreen(s1, 50);
|
||||
const screen2 = readScreen(s2, 50);
|
||||
|
||||
assert.ok(screen1.includes(`S1_${m1}`), `Surface 1 missing marker. Got:\n${screen1}`);
|
||||
assert.ok(screen2.includes(`S2_${m2}`), `Surface 2 missing marker. Got:\n${screen2}`);
|
||||
});
|
||||
|
||||
it("writes output to a file and verifies via surface", async () => {
|
||||
const surface = createTrackedSurface(env, "file-test");
|
||||
await sleep(1000);
|
||||
|
||||
const marker = uniqueId();
|
||||
const filePath = `/tmp/pi-mux-test-${marker}.txt`;
|
||||
|
||||
sendCommand(surface, `echo "FILE_${marker}" > ${filePath} && echo "WRITTEN_${marker}"`);
|
||||
|
||||
await waitForScreen(surface, new RegExp(`WRITTEN_${marker}`), 10_000, 50);
|
||||
const content = await waitForFile(filePath, 10_000, new RegExp(`FILE_${marker}`));
|
||||
assert.ok(content.includes(`FILE_${marker}`), `File content wrong. Got: ${content}`);
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
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");
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user