139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
/**
|
|
* @file pty-run.test.js
|
|
* @description Unit tests for the tmux-backed run lifecycle. Injects a fake
|
|
* tmux exec implementation (via tmux.js's test seam) so no real tmux binary
|
|
* is invoked.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
const { describe, it, beforeEach, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const path = require("node:path");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "pty-run-test-"));
|
|
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
|
|
|
|
const tmux = require("../lib/tmux");
|
|
const pty = require("../lib/pty-run");
|
|
|
|
describe("pty-run", () => {
|
|
after(() => {
|
|
try {
|
|
fs.rmSync(TMP, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
beforeEach(() => {
|
|
tmux.__reset();
|
|
});
|
|
|
|
it("spawnRun creates a new tmux session named ccam-lane-<id> when none exists", () => {
|
|
const calls = [];
|
|
tmux.__setExecImpl((args) => {
|
|
calls.push(args);
|
|
if (args[0] === "has-session") {
|
|
const e = new Error("no such session");
|
|
e.status = 1;
|
|
throw e;
|
|
}
|
|
return "";
|
|
});
|
|
const handle = pty.spawnRun({ laneId: 42, cwd: "/tmp/repo", model: "opus" });
|
|
assert.equal(handle.id, "ccam-lane-42");
|
|
const newSessionCall = calls.find((c) => c[0] === "new-session");
|
|
assert.ok(newSessionCall, "expected a new-session call");
|
|
assert.deepEqual(newSessionCall.slice(0, 6), [
|
|
"new-session",
|
|
"-d",
|
|
"-s",
|
|
"ccam-lane-42",
|
|
"-c",
|
|
"/tmp/repo",
|
|
]);
|
|
assert.ok(newSessionCall.includes("claude"));
|
|
assert.ok(newSessionCall.includes("--model"));
|
|
assert.ok(newSessionCall.includes("opus"));
|
|
});
|
|
|
|
it("spawnRun is a no-op (adopts) when the tmux session already exists", () => {
|
|
const calls = [];
|
|
tmux.__setExecImpl((args) => {
|
|
calls.push(args);
|
|
return ""; // has-session succeeds → already running
|
|
});
|
|
const handle = pty.spawnRun({ laneId: 7, cwd: "/tmp/repo" });
|
|
assert.equal(handle.id, "ccam-lane-7");
|
|
assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
|
|
});
|
|
|
|
it("spawnRun with resumeSessionId passes --resume in argv", () => {
|
|
let newSessionArgv = null;
|
|
tmux.__setExecImpl((args) => {
|
|
if (args[0] === "has-session") {
|
|
const e = new Error("gone");
|
|
e.status = 1;
|
|
throw e;
|
|
}
|
|
if (args[0] === "new-session") newSessionArgv = args;
|
|
return "";
|
|
});
|
|
pty.spawnRun({ laneId: 1, cwd: "/tmp/repo", resumeSessionId: "abc12345" });
|
|
assert.ok(newSessionArgv.includes("--resume"));
|
|
assert.ok(newSessionArgv.includes("abc12345"));
|
|
});
|
|
|
|
it("spawnRun appends a positional initial prompt after argv flags", () => {
|
|
let newSessionArgv = null;
|
|
tmux.__setExecImpl((args) => {
|
|
if (args[0] === "has-session") {
|
|
const e = new Error("gone");
|
|
e.status = 1;
|
|
throw e;
|
|
}
|
|
if (args[0] === "new-session") newSessionArgv = args;
|
|
return "";
|
|
});
|
|
pty.spawnRun({ laneId: 3, cwd: "/tmp/repo", initialPrompt: "fix the bug" });
|
|
assert.equal(newSessionArgv[newSessionArgv.length - 1], "fix the bug");
|
|
});
|
|
|
|
it("killRun calls tmux kill-session with the run id", () => {
|
|
const calls = [];
|
|
tmux.__setExecImpl((args) => {
|
|
calls.push(args);
|
|
return "";
|
|
});
|
|
assert.equal(pty.killRun("ccam-lane-5"), true);
|
|
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "ccam-lane-5"));
|
|
});
|
|
|
|
it("listRuns reflects live tmux-session state, not cached memory", () => {
|
|
tmux.__setExecImpl((args) => {
|
|
if (args[0] === "list-sessions") return "ccam-lane-1\nccam-lane-2\n";
|
|
return "";
|
|
});
|
|
const first = pty.listRuns();
|
|
assert.deepEqual(first.map((r) => r.id).sort(), ["ccam-lane-1", "ccam-lane-2"]);
|
|
|
|
// Session killed out-of-band (not through killRun) — next list() call
|
|
// must self-correct, proving state is computed, not stored.
|
|
tmux.__setExecImpl((args) => {
|
|
if (args[0] === "list-sessions") return "ccam-lane-1\n";
|
|
return "";
|
|
});
|
|
const second = pty.listRuns();
|
|
assert.deepEqual(
|
|
second.map((r) => r.id),
|
|
["ccam-lane-1"]
|
|
);
|
|
});
|
|
|
|
it("laneIdFromRunId parses the numeric lane id back out", () => {
|
|
assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
|
|
assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
|
|
});
|
|
});
|