Adding support for herdr - agents in new tabs
This commit is contained in:
+51
-14
@@ -2,9 +2,9 @@
|
||||
* Integration test harness for pi-interactive-subagents.
|
||||
*
|
||||
* Provides utilities to:
|
||||
* - Detect whether tmux is available
|
||||
* - Detect the active terminal-surface backend
|
||||
* - Create isolated test environments with test agent definitions
|
||||
* - Start real pi sessions in tmux panes
|
||||
* - Start real pi sessions in Herdr tabs or tmux panes
|
||||
* - Poll for file creation and screen output
|
||||
* - Clean up panes and temp files after tests
|
||||
*/
|
||||
@@ -18,12 +18,13 @@ import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join, resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
isMuxAvailable,
|
||||
getMuxBackend,
|
||||
createSurface,
|
||||
createSurfaceSplit,
|
||||
sendCommand,
|
||||
@@ -31,10 +32,13 @@ import {
|
||||
readScreen,
|
||||
readScreenAsync,
|
||||
closeSurface,
|
||||
pollForExit,
|
||||
shellEscape,
|
||||
} from "../../pi-extension/subagents/tmux.ts";
|
||||
wrapWithCompletionFile,
|
||||
type MuxBackend,
|
||||
} from "../../pi-extension/subagents/surface.ts";
|
||||
|
||||
// Re-export tmux primitives for tests
|
||||
// Re-export backend-neutral surface primitives for tests
|
||||
export {
|
||||
createSurface,
|
||||
createSurfaceSplit,
|
||||
@@ -43,7 +47,9 @@ export {
|
||||
readScreen,
|
||||
readScreenAsync,
|
||||
closeSurface,
|
||||
pollForExit,
|
||||
shellEscape,
|
||||
wrapWithCompletionFile,
|
||||
};
|
||||
|
||||
// ── Paths ──
|
||||
@@ -68,27 +74,49 @@ const EXTENSION_SOURCE = join(PROJECT_ROOT, "pi-extension", "subagents", "index.
|
||||
// ── Configuration ──
|
||||
|
||||
/** Model used for integration tests. Override with PI_TEST_MODEL env var. */
|
||||
export const TEST_MODEL = process.env.PI_TEST_MODEL ?? "anthropic/claude-haiku-4-5";
|
||||
const activePiModel =
|
||||
process.env.PI_PROVIDER && process.env.PI_MODEL
|
||||
? `${process.env.PI_PROVIDER}/${process.env.PI_MODEL}`
|
||||
: undefined;
|
||||
export const TEST_MODEL =
|
||||
process.env.PI_TEST_MODEL ?? activePiModel ?? "anthropic/claude-haiku-4-5";
|
||||
|
||||
/** Per-test timeout in ms. Override with PI_TEST_TIMEOUT env var. */
|
||||
export const PI_TIMEOUT = Number(process.env.PI_TEST_TIMEOUT ?? "120000");
|
||||
|
||||
// ── Backend detection ──
|
||||
|
||||
/**
|
||||
* Detect whether tmux is available in the current environment.
|
||||
* Returns ["tmux"] or [].
|
||||
*/
|
||||
export function getAvailableBackends(): string[] {
|
||||
return isMuxAvailable() ? ["tmux"] : [];
|
||||
/** Return the one backend selected automatically for this process. */
|
||||
export function getAvailableBackends(): MuxBackend[] {
|
||||
const backend = getMuxBackend();
|
||||
return backend ? [backend] : [];
|
||||
}
|
||||
|
||||
function parseHerdrJson(command: string, args: string[]): any {
|
||||
const output = execFileSync(command, args, { encoding: "utf8" });
|
||||
return JSON.parse(output);
|
||||
}
|
||||
|
||||
export function focusSurface(surface: string): void {
|
||||
if (getMuxBackend() === "herdr") {
|
||||
const pane = parseHerdrJson("herdr", ["pane", "get", surface]);
|
||||
const tabId = pane.result?.pane?.tab_id;
|
||||
if (!tabId) throw new Error(`Herdr pane ${surface} has no tab id`);
|
||||
execFileSync("herdr", ["tab", "focus", tabId], { encoding: "utf8" });
|
||||
return;
|
||||
}
|
||||
execFileSync("tmux", ["select-pane", "-t", surface], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
export function getFocusedSurface(): string | null {
|
||||
try {
|
||||
if (getMuxBackend() === "herdr") {
|
||||
const workspace = process.env.HERDR_WORKSPACE_ID;
|
||||
if (!workspace) return null;
|
||||
const response = parseHerdrJson("herdr", ["pane", "list", "--workspace", workspace]);
|
||||
return response.result?.panes?.find((pane: any) => pane.focused)?.pane_id ?? null;
|
||||
}
|
||||
|
||||
const panes = execFileSync("tmux", ["list-panes", "-F", "#{pane_id} #{pane_active}"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
@@ -110,7 +138,7 @@ export async function waitForFocusedSurface(
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Timeout (${timeout}ms) waiting for focused tmux pane ${surface}; ` +
|
||||
`Timeout (${timeout}ms) waiting for focused terminal surface ${surface}; ` +
|
||||
`current focus is ${getFocusedSurface() ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
@@ -139,7 +167,13 @@ export function createTestEnv(): TestEnv {
|
||||
if (existsSync(TEST_AGENTS_SRC)) {
|
||||
for (const file of readdirSync(TEST_AGENTS_SRC)) {
|
||||
if (file.endsWith(".md")) {
|
||||
cpSync(join(TEST_AGENTS_SRC, file), join(agentsDir, file));
|
||||
const destination = join(agentsDir, file);
|
||||
cpSync(join(TEST_AGENTS_SRC, file), destination);
|
||||
const definition = readFileSync(destination, "utf8").replace(
|
||||
/^model:\s*.+$/m,
|
||||
`model: ${TEST_MODEL}`,
|
||||
);
|
||||
writeFileSync(destination, definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +261,9 @@ export function startPi(
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
// Herdr does not retain terminal output for tabs that have never been shown.
|
||||
// Focus the test runner surface so waitForScreen can observe its transcript.
|
||||
focusSurface(surface);
|
||||
sendLongCommand(surface, `${cmd}; echo '__TEST_DONE_'$?'__'`, {
|
||||
scriptPath: join(testDir, `test-launch-${Date.now()}.sh`),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user