fix(tmux,run,hooks): the four pre-existing bugs the #37 evaluation found

- tmux targets are exact (`=name`, `=name:` for panes/options): a bare
  `-t agenthub-lane-1` resolves by prefix to agenthub-lane-12 once lane 1
  is gone, so killing a finished lane could kill another.
- getRun/killRun refuse ids that are not `agenthub-lane-<n>`, so
  DELETE /api/run/main can no longer kill a user's own tmux session.
- router/apply reports restart_required only for a running pane; getRun
  returns a `gone` handle, so it was always true.
- needs_action is cleared only by the session that raised it (new nullable
  lanes.needs_action_session); a second session in the same cwd used to
  clear it on its second hook.
This commit is contained in:
2026-09-23 13:13:16 +07:00
parent 04b6b98d97
commit 99a2a8277c
18 changed files with 134 additions and 33 deletions
+15 -5
View File
@@ -856,9 +856,9 @@ describe("destructive lane lifecycle actions", () => {
// Mock tmux so has-session always returns 0 (session exists), simulating a stuck session
// Same shape as tmux.js's own exec: sync, argv only, throws on failure.
tmux.__setExecImpl((args) => {
if (args[0] === "has-session" && args[2] === runId) return "";
if (args[0] === "has-session" && args[2] === `=${runId}`) return "";
if (args[0] === "list-sessions") return `${runId}\n`;
if (args[0] === "kill-session" && args[2] === runId) return "";
if (args[0] === "kill-session" && args[2] === `=${runId}`) return "";
throw Object.assign(new Error("mocked tmux failure"), { status: 1 });
});
@@ -949,7 +949,12 @@ describe("destructive lane lifecycle actions", () => {
// Mock tmux so the session appears live
tmux.__setExecImpl(async (cmd, args) => {
if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
if (
cmd === "tmux" &&
args[0] === "has-session" &&
args[1] === "-t" &&
args[2] === `=${runId}`
) {
return { code: 0, stdout: "", stderr: "" };
}
if (cmd === "tmux" && args[0] === "list-sessions") {
@@ -1218,7 +1223,12 @@ describe("lane ensure, start mode, lane_id and releasing a finished run", () =>
// Mock tmux so the run appears to be live.
tmux.__setExecImpl(async (cmd, args) => {
if (cmd === "tmux" && args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
if (
cmd === "tmux" &&
args[0] === "has-session" &&
args[1] === "-t" &&
args[2] === `=${runId}`
) {
return { code: 0, stdout: "", stderr: "" };
}
if (cmd === "tmux" && args[0] === "list-sessions") {
@@ -1264,7 +1274,7 @@ describe("lane ensure, start mode, lane_id and releasing a finished run", () =>
// Mock tmux so the session appears to be gone (has-session fails with status 1).
tmux.__setExecImpl((args) => {
if (args[0] === "has-session" && args[1] === "-t" && args[2] === runId) {
if (args[0] === "has-session" && args[1] === "-t" && args[2] === `=${runId}`) {
const e = new Error("no such session");
e.status = 1;
throw e;