Integrating swarm-forge into project

This commit is contained in:
Chris Troutner
2026-08-25 14:50:21 -07:00
parent f6e987b135
commit a8230982a4
53 changed files with 0 additions and 165 deletions
+285
View File
@@ -0,0 +1,285 @@
(ns swarmforge.handoff-test
(:require [babashka.fs :as fs]
[clojure.java.shell :as sh]
[clojure.string :as str]
[clojure.test :refer [deftest is run-tests testing use-fixtures]]))
(def repo-root (fs/cwd))
(def scripts-dir (fs/path repo-root "swarmforge" "scripts"))
(def temp-dirs (atom []))
(use-fixtures :once
(fn [tests]
(try
(tests)
(finally
(doseq [dir @temp-dirs]
(fs/delete-tree dir))))))
(defn script [name]
(str (fs/path scripts-dir name)))
(defn tmp-dir []
(let [dir (fs/create-temp-dir {:prefix "swarmforge-handoff-test."})]
(swap! temp-dirs conj dir)
dir))
(defn run
[{:keys [dir env ok?]} & args]
(let [result (apply sh/sh (concat args [:dir (str dir)
:env (merge {"PATH" (System/getenv "PATH")
"GIT_CONFIG_NOSYSTEM" "1"}
env)]))]
(when (and (not (false? ok?)) (not= 0 (:exit result)))
(throw (ex-info (str "Command failed: " (str/join " " args))
(assoc result :args args))))
result))
(defn write-file [path text]
(fs/create-dirs (fs/parent path))
(spit (str path) text))
(defn read-file [path]
(slurp (str path)))
(defn init-repo! [root]
(run {:dir root} "git" "init" "-q")
(run {:dir root} "git" "config" "user.email" "test@example.com")
(run {:dir root} "git" "config" "user.name" "Test User")
(write-file (fs/path root "README.md") "initial\n")
(run {:dir root} "git" "add" "README.md")
(run {:dir root} "git" "commit" "-q" "-m" "Initial commit")
(str/trim (:out (run {:dir root} "git" "rev-parse" "--short=10" "HEAD"))))
(defn setup-project!
([root] (setup-project! root {"sender" "task" "receiver" "task"}))
([root roles]
(doseq [dir [".swarmforge/handoffs/outbox/tmp"
".swarmforge/handoffs/sent"
".swarmforge/handoffs/failed"
".swarmforge/handoffs/inbox/new"
".swarmforge/handoffs/inbox/in_process"
".swarmforge/handoffs/inbox/completed"]]
(fs/create-dirs (fs/path root dir)))
(write-file
(fs/path root ".swarmforge/roles.tsv")
(apply str
(for [[role mode] roles]
(format "%s\tmaster\t%s\tsession\t%s\tcodex\t%s\n"
role root (str/capitalize role) mode))))))
(defn handoff
[{:keys [id from to recipient priority type task commit body
enqueued-at dequeued-at completed-at]}]
(str "id: " id "\n"
"from: " from "\n"
"to: " to "\n"
(when recipient (str "recipient: " recipient "\n"))
"priority: " priority "\n"
"type: " type "\n"
(when task (str "task: " task "\n"))
(when commit (str "commit: " commit "\n"))
(when enqueued-at (str "enqueued_at: " enqueued-at "\n"))
(when dequeued-at (str "dequeued_at: " dequeued-at "\n"))
(when completed-at (str "completed_at: " completed-at "\n"))
"\n"
(or body (str "payload for " id)) "\n"))
(defn handoff-path [root state filename]
(fs/path root ".swarmforge" "handoffs" "inbox" state filename))
(defn put-handoff! [root state filename attrs]
(let [path (handoff-path root state filename)]
(write-file path (handoff attrs))
path))
(defn header [path field]
(some->> (str/split-lines (read-file path))
(take-while seq)
(some (fn [line]
(let [prefix (str field ": ")]
(when (str/starts-with? line prefix)
(subs line (count prefix))))))))
(defn make-queued-handoff!
([root filename attrs]
(put-handoff! root "new" filename
(merge {:from "sender"
:to "receiver"
:recipient "receiver"
:priority "50"
:type "git_handoff"
:task "task-one"
:commit "0123456789"
:body "merge_and_process sender 0123456789"}
attrs))))
(deftest swarm-handoff-validates-and-queues-git-handoffs
(let [root (tmp-dir)
commit (init-repo! root)]
(setup-project! root)
(testing "git_handoff requires a task name"
(let [draft (fs/path root "tmp" "missing-task.handoff")]
(write-file draft (format "type: git_handoff\nto: receiver\npriority: 50\ncommit: %s\n" commit))
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "sender"} :ok? false}
(script "swarm_handoff.sh") (str draft))]
(is (= 2 (:exit result)))
(is (str/includes? (:err result) "Missing required header 'task'"))
(is (fs/exists? draft)))))
(testing "valid git_handoff writes task, canonical commit, and generated payload"
(let [draft (fs/path root "tmp" "valid.handoff")]
(write-file draft (format "type: git_handoff\nto: receiver\npriority: 50\ntask: task-1-cave-setup\ncommit: %s\n" commit))
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "sender"}}
(script "swarm_handoff.sh") (str draft))
queued (-> (:out result) str/trim (str/replace #"^HANDOFF QUEUED: " ""))
content (read-file queued)]
(is (str/includes? content "task: task-1-cave-setup\n"))
(is (str/includes? content (str "commit: " commit "\n")))
(is (str/includes? content (str "merge_and_process sender " commit)))
(is (fs/exists? queued))
(is (not (fs/exists? draft))))))))
(deftest ready-for-next-task-accepts-and-resumes-single-tasks
(let [root (tmp-dir)]
(init-repo! root)
(setup-project! root {"receiver" "task"})
(testing "accepts one queued task and prints task name"
(make-queued-handoff! root "50_20260615T000001Z_000001_from_sender_to_receiver.handoff"
{:id "20260615T000001Z_000001_from_sender"
:task "task-alpha"})
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"}}
(script "ready_for_next.sh"))
out (:out result)
in-process (fs/path root ".swarmforge/handoffs/inbox/in_process/50_20260615T000001Z_000001_from_sender_to_receiver.handoff")]
(is (str/includes? out "TASK:"))
(is (str/includes? out "TASK_NAME: task-alpha"))
(is (fs/exists? in-process))
(is (some? (header in-process "dequeued_at")))))
(testing "returns existing in-process task before queued tasks"
(make-queued-handoff! root "40_20260615T000002Z_000002_from_sender_to_receiver.handoff"
{:id "20260615T000002Z_000002_from_sender"
:priority "40"
:task "task-beta"})
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"}}
(script "ready_for_next.sh"))]
(is (str/includes? (:out result) "task-alpha"))
(is (fs/exists? (fs/path root ".swarmforge/handoffs/inbox/new/40_20260615T000002Z_000002_from_sender_to_receiver.handoff")))))))
(deftest ready-for-next-batch-groups-equal-priority-handoffs
(let [root (tmp-dir)]
(init-repo! root)
(setup-project! root {"receiver" "batch"})
(make-queued-handoff! root "10_20260615T000001Z_000001_from_sender_to_receiver.handoff"
{:id "20260615T000001Z_000001_from_sender" :priority "10" :task "task-a"})
(make-queued-handoff! root "10_20260615T000002Z_000002_from_sender_to_receiver.handoff"
{:id "20260615T000002Z_000002_from_sender" :priority "10" :task "task-b"})
(make-queued-handoff! root "20_20260615T000003Z_000003_from_sender_to_receiver.handoff"
{:id "20260615T000003Z_000003_from_sender" :priority "20" :task "task-c"})
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"}}
(script "ready_for_next.sh"))
out (:out result)
batch-dir (->> (str/split-lines out)
(filter #(str/starts-with? % "BATCH: "))
first
(#(subs % 7)))]
(is (str/includes? out "COUNT: 2"))
(is (str/includes? out "TASK_NAME: task-a"))
(is (str/includes? out "TASK_NAME: task-b"))
(is (not (str/includes? out "TASK_NAME: task-c")))
(is (= 2 (count (fs/glob batch-dir "*.handoff"))))
(is (fs/exists? (fs/path root ".swarmforge/handoffs/inbox/new/20_20260615T000003Z_000003_from_sender_to_receiver.handoff"))))))
(deftest done-with-current-task-completes-and-accepts-next-task
(let [root (tmp-dir)]
(init-repo! root)
(setup-project! root {"receiver" "task"})
(put-handoff! root "in_process" "50_20260615T000001Z_000001_from_sender_to_receiver.handoff"
{:id "20260615T000001Z_000001_from_sender"
:from "sender" :to "receiver" :recipient "receiver"
:priority "50" :type "git_handoff" :task "task-current"
:commit "0123456789"})
(make-queued-handoff! root "50_20260615T000002Z_000002_from_sender_to_receiver.handoff"
{:id "20260615T000002Z_000002_from_sender"
:task "task-next"})
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"}}
(script "done_with_current.sh"))
completed (fs/path root ".swarmforge/handoffs/inbox/completed/50_20260615T000001Z_000001_from_sender_to_receiver.handoff")
next-file (fs/path root ".swarmforge/handoffs/inbox/in_process/50_20260615T000002Z_000002_from_sender_to_receiver.handoff")]
(is (str/includes? (:out result) "COMPLETED:"))
(is (str/includes? (:out result) "TASK_NAME: task-next"))
(is (some? (header completed "completed_at")))
(is (some? (header next-file "dequeued_at"))))))
(deftest done-with-current-batch-completes-and-accepts-next-batch
(let [root (tmp-dir)
batch (fs/path root ".swarmforge/handoffs/inbox/in_process/batch_20260615T000001Z_000001")]
(init-repo! root)
(setup-project! root {"receiver" "batch"})
(fs/create-dirs batch)
(write-file (fs/path batch "10_20260615T000001Z_000001_from_sender_to_receiver.handoff")
(handoff {:id "20260615T000001Z_000001_from_sender"
:from "sender" :to "receiver" :recipient "receiver"
:priority "10" :type "git_handoff" :task "task-a"
:commit "0123456789"}))
(write-file (fs/path batch "10_20260615T000002Z_000002_from_sender_to_receiver.handoff")
(handoff {:id "20260615T000002Z_000002_from_sender"
:from "sender" :to "receiver" :recipient "receiver"
:priority "10" :type "git_handoff" :task "task-b"
:commit "0123456789"}))
(make-queued-handoff! root "20_20260615T000003Z_000003_from_sender_to_receiver.handoff"
{:id "20260615T000003Z_000003_from_sender"
:priority "20"
:task "task-c"})
(let [result (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"}}
(script "done_with_current.sh"))
completed-batch (fs/path root ".swarmforge/handoffs/inbox/completed/batch_20260615T000001Z_000001")]
(is (str/includes? (:out result) "COMPLETED_BATCH:"))
(is (str/includes? (:out result) "TASK_NAME: task-c"))
(is (= 2 (count (fs/glob completed-batch "*.handoff"))))
(is (every? #(some? (header % "completed_at"))
(fs/glob completed-batch "*.handoff"))))))
(deftest stop-handoff-daemon-stops-running-process-and-removes-pid-file
(let [root (tmp-dir)]
(init-repo! root)
(fs/create-dirs (fs/path root ".swarmforge/daemon"))
(write-file (fs/path root ".swarmforge/roles.tsv")
(str "coder\tmaster\t" root "\tsession\tCoder\tcodex\ttask\n"))
(write-file (fs/path root ".swarmforge/tmux-socket") "/tmp/fake.sock\n")
(run {:dir root :ok? false}
"sh" "-c"
(str "bb " (script "handoffd.bb") " " root " >/dev/null 2>&1 &"))
(Thread/sleep 1500)
(let [pid-file (fs/path root ".swarmforge/daemon/handoffd.pid")]
(is (fs/exists? pid-file))
(let [pid (str/trim (read-file pid-file))
stop (run {:dir root} (script "stop_handoff_daemon.bb") (str root))]
(is (= 0 (:exit stop)))
(Thread/sleep 300)
(is (not (fs/exists? pid-file)))
(is (not= 0 (:exit (run {:dir root :ok? false} "kill" "-0" pid))))))))
(deftest helpers-refuse-wrong-current-work-shape
(let [root (tmp-dir)
batch (fs/path root ".swarmforge/handoffs/inbox/in_process/batch_20260615T000001Z_000001")]
(init-repo! root)
(setup-project! root {"receiver" "batch"})
(fs/create-dirs batch)
(write-file (fs/path batch "10_20260615T000001Z_000001_from_sender_to_receiver.handoff")
(handoff {:id "20260615T000001Z_000001_from_sender"
:from "sender" :to "receiver" :recipient "receiver"
:priority "10" :type "git_handoff" :task "task-a"
:commit "0123456789"}))
(testing "task helpers refuse an in-process batch"
(let [ready (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"} :ok? false}
(script "ready_for_next_task.sh"))
done (run {:dir root :env {"SWARMFORGE_ROLE" "receiver"} :ok? false}
(script "done_with_current_task.sh"))]
(is (= 2 (:exit ready)))
(is (str/includes? (:err ready) "TASK_IN_PROCESS_IS_BATCH"))
(is (= 2 (:exit done)))
(is (str/includes? (:err done) "CURRENT_WORK_IS_BATCH"))))))
(defn -main [& _]
(let [{:keys [fail error]} (run-tests 'swarmforge.handoff-test)]
(System/exit (+ fail error))))
+350
View File
@@ -0,0 +1,350 @@
(ns swarmforge.script-test
(:require [babashka.fs :as fs]
[clojure.java.shell :as sh]
[clojure.string :as str]
[clojure.test :refer [deftest is testing]]))
(def repo-root (fs/cwd))
(def scripts-dir (fs/path repo-root "swarmforge" "scripts"))
(defn write-file [path text]
(fs/create-dirs (fs/parent path))
(spit (str path) text))
(defn run
[{:keys [dir env ok?]} & args]
(let [result (apply sh/sh (concat args [:dir (str dir)
:env (merge {"PATH" (System/getenv "PATH")
"GIT_CONFIG_NOSYSTEM" "1"}
env)]))]
(when (and (not (false? ok?)) (not= 0 (:exit result)))
(throw (ex-info (str "Command failed: " (str/join " " args))
(assoc result :args args))))
result))
(defn init-repo! [root]
(run {:dir root} "git" "init" "-q")
(run {:dir root} "git" "config" "user.email" "test@example.com")
(run {:dir root} "git" "config" "user.name" "Test User")
(write-file (fs/path root "README.md") "initial\n")
(run {:dir root} "git" "add" "README.md")
(run {:dir root} "git" "commit" "-q" "-m" "Initial commit"))
(defn tmp-dir []
(fs/create-temp-dir {:prefix "swarmforge-script-test."}))
(defn script [name]
(str (fs/path scripts-dir name)))
(deftest handoff-lib-parses-and-prints-handoff-files
(let [root (tmp-dir)
handoff-file (fs/path root "task.handoff")]
(try
(write-file handoff-file
(str "id: 1\n"
"from: coder\n"
"to: cleaner\n"
"priority: 10\n"
"type: git_handoff\n"
"task: task-alpha\n"
"\n"
"merge_and_process coder abcdef1234\n"))
(let [header (run {:dir root} (script "handoff_lib.bb") "header-field" "task.handoff" "task")
body (run {:dir root} (script "handoff_lib.bb") "body" "task.handoff")
task (run {:dir root} (script "handoff_lib.bb") "print-task" "task.handoff")]
(is (str/includes? (:out header) "task-alpha"))
(is (str/includes? (:out body) "merge_and_process coder abcdef1234"))
(is (str/includes? (:out task) "TASK: task.handoff"))
(is (str/includes? (:out task) "FROM: coder"))
(is (str/includes? (:out task) "TASK_NAME: task-alpha")))
(finally
(fs/delete-tree root)))))
(deftest handoff-lib-updates-headers-and-reads-role-state
(let [root (tmp-dir)]
(try
(init-repo! root)
(write-file (fs/path root ".swarmforge/roles.tsv")
(str "coder\tmaster\t" root "\tsession\tCoder\tcodex\ttask\n"
"cleaner\tcleaner\t" root "/.worktrees/cleaner\tsession\tCleaner\tcodex\tbatch\n"))
(write-file (fs/path root ".swarmforge/handoffs/inbox/new/item.handoff")
(str "id: 1\n"
"from: coder\n"
"to: cleaner\n"
"priority: 20\n"
"type: note\n"
"\n"
"payload\n"))
(run {:dir root} (script "handoff_lib.bb") "role-known" "cleaner")
(run {:dir root} (script "handoff_lib.bb") "set-header" ".swarmforge/handoffs/inbox/new/item.handoff" "dequeued_at" "2026-06-16T00:00:00Z")
(let [mode (run {:dir root} (script "handoff_lib.bb") "role-receive-mode" "cleaner")
worktree (run {:dir root} (script "handoff_lib.bb") "role-worktree-name" "cleaner")
dequeued (run {:dir root} (script "handoff_lib.bb") "header-field" ".swarmforge/handoffs/inbox/new/item.handoff" "dequeued_at")
seq-1 (run {:dir root} (script "handoff_lib.bb") "next-sequence")
seq-2 (run {:dir root} (script "handoff_lib.bb") "next-sequence")]
(is (str/includes? (:out mode) "batch"))
(is (str/includes? (:out worktree) "cleaner"))
(is (str/includes? (:out dequeued) "2026-06-16T00:00:00Z"))
(is (str/includes? (:out seq-1) "000001"))
(is (str/includes? (:out seq-2) "000002")))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-launcher-parses-config-and-writes-state-files
(let [root (tmp-dir)]
(try
(write-file (fs/path root "swarmforge/constitution.prompt")
"Read articles.\n")
(write-file (fs/path root "swarmforge/swarmforge.conf")
(str "# comment\n"
"window coder codex master\n"
"window cleaner codex cleaner batch\n"))
(write-file (fs/path root "swarmforge/roles/coder.prompt") "coder\n")
(write-file (fs/path root "swarmforge/roles/cleaner.prompt") "cleaner\n")
(let [result (run {:dir root} (script "swarmforge.bb") "--test-parse" (str root))]
(is (str/includes? (:out result) "coder Coder"))
(is (str/includes? (:out result) "cleaner Cleaner"))
(is (str/includes? (:out result) "cleaner batch"))
(is (str/includes? (:out result) "swarmforge-coder"))
(is (str/includes? (:out result) "swarmforge-cleaner"))
(is (fs/exists? (fs/path root ".swarmforge/tmux-socket"))))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-uses-portable-tmux-socket-dir
(let [root (tmp-dir)]
(try
(write-file (fs/path root "swarmforge/constitution.prompt")
"Read articles.\n")
(write-file (fs/path root "swarmforge/swarmforge.conf")
"window coder codex master\n")
(write-file (fs/path root "swarmforge/roles/coder.prompt") "coder\n")
(run {:dir root} (script "swarmforge.bb") "--test-parse" (str root))
(let [socket-path (str/trim (slurp (str (fs/path root ".swarmforge/tmux-socket"))))]
(is (str/starts-with? socket-path "/tmp/swarmforge-"))
(is (not (str/starts-with? socket-path "/private/tmp/"))))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-launcher-rejects-invalid-config
(let [root (tmp-dir)]
(try
(write-file (fs/path root "swarmforge/constitution.prompt")
"Read articles.\n")
(write-file (fs/path root "swarmforge/swarmforge.conf")
(str "window coder codex master\n"
"window coder codex other\n"))
(write-file (fs/path root "swarmforge/roles/coder.prompt") "coder\n")
(let [result (run {:dir root :ok? false} (script "swarmforge.bb") "--test-parse" (str root))]
(is (= 1 (:exit result)))
(is (str/includes? (:err result) "Duplicate role 'coder'")))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-terminal-bridge-preserves-adapter-globals
(let [root (tmp-dir)]
(try
(write-file (fs/path root "swarmforge/scripts/swarm-terminal-adapter.sh")
(str "load_terminal_backend() {\n"
" source \"$SCRIPT_DIR/terminal-adapters/$1.sh\"\n"
"}\n"))
(write-file (fs/path root "swarmforge/scripts/terminal-adapters/probe.sh")
(str "terminal_open_session() {\n"
" printf '%s\\n' \"$WORKING_DIR|$TMUX_SOCKET|$1|$2|$3\"\n"
"}\n"))
(let [result (run {:dir root}
(script "swarmforge.bb")
"--test-terminal-bridge"
(str root)
"probe")]
(is (str/includes? (:out result) (str root "|")))
(is (str/includes? (:out result) "|swarmforge-specifier|SwarmForge Specifier|"))
(is (not (str/includes? (:out result) "cd ''")))
(is (not (str/includes? (:out result) "-S ''"))))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-agent-start-delay-is-configurable
(let [default-result (run {:dir repo-root}
(script "swarmforge.bb")
"--test-agent-start-delay")
configured-result (run {:dir repo-root
:env {"SWARMFORGE_AGENT_START_DELAY_MS" "2750"}}
(script "swarmforge.bb")
"--test-agent-start-delay")
invalid-result (run {:dir repo-root
:env {"SWARMFORGE_AGENT_START_DELAY_MS" "fast"}}
(script "swarmforge.bb")
"--test-agent-start-delay")]
(is (= "1500" (str/trim (:out default-result))))
(is (= "2750" (str/trim (:out configured-result))))
(is (= "1500" (str/trim (:out invalid-result))))))
(deftest swarmforge-sleep-prevention-can-be-disabled
(let [result (run {:dir repo-root
:env {"SWARMFORGE_PREVENT_SLEEP" "0"}}
(script "swarmforge.bb")
"--test-sleep-inhibitor-prefix")]
(is (= "" (str/trim (:out result))))))
(deftest swarmforge-launcher-parses-extra-cli-args
(let [root (tmp-dir)]
(try
(write-file (fs/path root "swarmforge/constitution.prompt")
"Read articles.\n")
(write-file (fs/path root "swarmforge/swarmforge.conf")
(str "window coder copilot master --yolo\n"
"window cleaner copilot cleaner batch --allow-all-tools\n"))
(write-file (fs/path root "swarmforge/roles/coder.prompt") "coder\n")
(write-file (fs/path root "swarmforge/roles/cleaner.prompt") "cleaner\n")
(let [result (run {:dir root} (script "swarmforge.bb") "--test-parse" (str root))]
(is (str/includes? (:out result) "coder Coder"))
(is (str/includes? (:out result) "task --yolo"))
(is (str/includes? (:out result) "batch --allow-all-tools")))
(finally
(fs/delete-tree root)))))
(deftest copilot-launch-command-passes-extra-cli-args
(let [root (tmp-dir)]
(try
(let [result (run {:dir root}
(script "swarmforge.bb")
"--test-launch-command"
(str root)
"copilot"
"--yolo")
command (:out result)]
(is (str/includes? command "copilot -C "))
(is (re-find #"--name 'SwarmForge Coder' --yolo -i" command)))
(finally
(fs/delete-tree root)))))
(deftest grok-launch-command-passes-initial-prompt
(let [root (tmp-dir)]
(try
(let [result (run {:dir root}
(script "swarmforge.bb")
"--test-launch-command"
(str root)
"grok")
command (:out result)]
(is (str/includes? command "grok --cwd "))
(is (str/includes? command "--permission-mode acceptEdits"))
(is (str/includes? command "--rules \"$(cat "))
(is (str/includes? command "--verbatim \"$(cat "))
(is (str/includes? command ".swarmforge/prompts/coder.md"))
(is (fs/exists? (fs/path root ".swarmforge/prompts/coder.md"))))
(finally
(fs/delete-tree root)))))
(deftest grok-launch-command-uses-bypass-permissions-with-always-approve
(let [root (tmp-dir)]
(try
(let [result (run {:dir root}
(script "swarmforge.bb")
"--test-launch-command"
(str root)
"grok"
"--always-approve")
command (:out result)]
(is (str/includes? command "--permission-mode bypassPermissions"))
(is (str/includes? command "--always-approve"))
(is (not (str/includes? command "--permission-mode acceptEdits"))))
(finally
(fs/delete-tree root)))))
(deftest window-watchdog-rewrites-window-state-and-id-list
(let [root (tmp-dir)
state-file (fs/path root "windows.tsv")
ids-file (fs/path root "window-ids")]
(try
(write-file state-file
(str "1\told-a\tswarmforge-coder\tSwarmForge Coder\n"
"2\told-b\tswarmforge-cleaner\tSwarmForge Cleaner\n"))
(write-file ids-file "old-a\nold-b\n")
(run {:dir root} (script "swarm-window-watchdog.bb") "--rewrite-window-id" "windows.tsv" "window-ids" "2" "new-b")
(let [state (slurp (str state-file))
ids (slurp (str ids-file))]
(is (str/includes? state "1\told-a\tswarmforge-coder\tSwarmForge Coder"))
(is (str/includes? state "2\tnew-b\tswarmforge-cleaner\tSwarmForge Cleaner"))
(is (= "old-a\nnew-b\n" ids)))
(finally
(fs/delete-tree root)))))
(deftest swarmforge-detects-nonzero-pane-base-index
(let [root (tmp-dir)
sock (str root "/test.sock")
conf (fs/path root "tmux.conf")]
(try
(write-file conf "set -g base-index 1\nset -g pane-base-index 1\n")
(run {:dir root} "tmux" "-S" sock "-f" (str conf) "new-session" "-d" "-s" "probe" "sleep" "120")
(let [result (run {:dir root}
(script "swarmforge.bb")
"--test-tmux-base-indexes"
sock)]
(is (= "1 1" (str/trim (:out result)))))
(finally
(run {:dir root :ok? false} "tmux" "-S" sock "kill-server")
(fs/delete-tree root)))))
(deftest swarm-cleanup-tolerates-missing-runtime-state
(let [root (tmp-dir)
ids-file (fs/path root ".swarmforge/window-ids")]
(try
(write-file ids-file "window-a\nwindow-b\n")
(let [result (run {:dir root
:env {"SWARMFORGE_TERMINAL_BACKEND" "none"}}
(str (fs/path scripts-dir "swarm-cleanup.sh"))
"/tmp/nonexistent.sock"
(str ids-file))]
(is (= 0 (:exit result)))
(is (= "" (:err result))))
(finally
(fs/delete-tree root)))))
(defn close-swarm []
(str (fs/path repo-root "close-swarm")))
(deftest close-swarm-reports-when-no-swarm-state
(let [root (tmp-dir)]
(try
(let [result (run {:dir root :ok? false
:env {"SWARMFORGE_TERMINAL_BACKEND" "none"}}
(close-swarm)
(str root))]
(is (not= 0 (:exit result)))
(is (str/includes? (str (:err result) (:out result)) "No SwarmForge swarm")))
(finally
(fs/delete-tree root)))))
(deftest close-swarm-kills-tmux-sessions-and-stops-daemon
(let [root (tmp-dir)
sock (str (fs/path root "swarm.sock"))
pid-file (fs/path root ".swarmforge/daemon/handoffd.pid")
daemon (.start (java.lang.ProcessBuilder. ["sleep" "120"]))
pid (str (.pid daemon))]
(try
(write-file (fs/path root ".swarmforge/tmux-socket") (str sock "\n"))
(write-file (fs/path root ".swarmforge/sessions.tsv")
(str "1\tcoder\tswarmforge-coder\tCoder\tcodex\n"
"2\tcleaner\tswarmforge-cleaner\tCleaner\tcodex\n"))
(write-file (fs/path root ".swarmforge/window-ids") "win-a\nwin-b\n")
(write-file pid-file (str pid "\n"))
(run {:dir root} "tmux" "-S" sock "new-session" "-d" "-s" "swarmforge-coder" "sleep" "120")
(run {:dir root} "tmux" "-S" sock "new-session" "-d" "-s" "swarmforge-cleaner" "sleep" "120")
(let [result (run {:dir root
:env {"SWARMFORGE_TERMINAL_BACKEND" "none"}}
(close-swarm)
(str root))]
(is (= 0 (:exit result)))
(is (not= 0 (:exit (run {:dir root :ok? false}
"tmux" "-S" sock "has-session" "-t" "swarmforge-coder"))))
(is (not= 0 (:exit (run {:dir root :ok? false}
"tmux" "-S" sock "has-session" "-t" "swarmforge-cleaner"))))
(is (not (fs/exists? pid-file)))
(is (false? (.isAlive daemon))))
(finally
(when (.isAlive daemon)
(.destroyForcibly daemon))
(run {:dir root :ok? false} "tmux" "-S" sock "kill-server")
(fs/delete-tree root)))))