mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-client.git
synced 2026-09-21 16:52:02 -07:00
Integrating swarm-forge into project
This commit is contained in:
@@ -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))))
|
||||
@@ -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)))))
|
||||
Reference in New Issue
Block a user