fix: sprites session

This commit is contained in:
Tienson Qin
2026-02-07 16:35:06 +08:00
parent e455f4fd39
commit 99a5ce5b8b
4 changed files with 563 additions and 53 deletions

View File

@@ -150,6 +150,13 @@
(fn []
(set! (.-runtime-events-stream self) nil)))))))
(defn- start-runtime-events-stream-background! [^js self session-id runtime]
;; Fire-and-forget start. Returning nil avoids p/let awaiting the stream task.
(when (and (map? runtime)
(string? (:session-id runtime)))
(start-runtime-events-stream! self session-id runtime)
nil))
(defn- <transition! [^js self to-status event-type data]
(p/let [session (<get-session self)]
(cond
@@ -190,9 +197,9 @@
:sprite-name (:sprite-name runtime)}
:ts (common/now-ms)})]
(p/let [_ (<put-session! self session)
_ (<put-events! self events)
_ (when-not (terminal-status? (:status session))
(start-runtime-events-stream! self (:id session) runtime))]
_ (<put-events! self events)]
(when-not (terminal-status? (:status session))
(start-runtime-events-stream-background! self (:id session) runtime))
runtime))))))
(defn- handle-init [^js self request]
@@ -205,7 +212,7 @@
session (<get-session self)]
(when (and (map? (:runtime session))
(not (terminal-status? (:status session))))
(start-runtime-events-stream! self session-id (:runtime session)))
(start-runtime-events-stream-background! self session-id (:runtime session)))
(http/json-response :sessions/create
{:session-id session-id
:status (:status session)
@@ -301,21 +308,20 @@
(p/let [_ (when (and runtime
provider
(string? (:session-id runtime)))
(do
(start-runtime-events-stream! self (:id current-session) runtime)
(-> (runtime-provider/<send-message! provider
runtime
{:message message
:kind (:kind body)})
(.catch (fn [error]
(log/error :agent/runtime-message-error
{:session-id (:id current-session)
:runtime-session-id (:session-id runtime)
:error error})
(<append-event! self {:type "agent.runtime.error"
:data {:session-id (:id current-session)
:message (str error)}
:ts (common/now-ms)}))))))]
(start-runtime-events-stream! self (:id current-session) runtime)
(-> (runtime-provider/<send-message! provider
runtime
{:message message
:kind (:kind body)})
(.catch (fn [error]
(log/error :agent/runtime-message-error
{:session-id (:id current-session)
:runtime-session-id (:session-id runtime)
:error error})
(<append-event! self {:type "agent.runtime.error"
:data {:session-id (:id current-session)
:message (str error)}
:ts (common/now-ms)})))))]
(http/json-response :sessions/message {:ok true})))))))))))
(defn- handle-cancel [^js self request]
@@ -344,7 +350,7 @@
_ (when (and runtime
provider
(string? (:session-id runtime)))
(start-runtime-events-stream! self (:id current-session) runtime))
(start-runtime-events-stream-background! self (:id current-session) runtime))
_ (when (and runtime
provider
(string? (:session-id runtime)))

View File

@@ -99,8 +99,8 @@
(defn- ->base64
[value]
(when (string? value)
(let [bytes (.encode (js/TextEncoder.) value)
binary (apply str (map char bytes))]
(let [payload (.encode (js/TextEncoder.) value)
binary (apply str (map char payload))]
(js/btoa binary))))
(defn- curl-auth-arg [token]
@@ -183,8 +183,30 @@
(let [base (sprites-base-url env)
token (sprites-token env)
q (build-query (map (fn [c] [:cmd c]) cmd-vec))
url (sprites-http-url base (str "/v1/sprites/" sprite-name "/exec?" q))]
(fetch-json! "POST" url {:token token})))
url (sprites-http-url base (str "/v1/sprites/" sprite-name "/exec?" q))
^js headers (js/Headers.)]
(.set headers "Accept" "application/json")
(when token (token-header! headers token))
(p/let [resp (js/fetch url #js {:method "POST" :headers headers})
status (.-status resp)
text (->promise (.text resp))
parsed (parse-json-safe text)]
(when-not (<= 200 status 299)
(throw (ex-info "sprites exec failed"
{:status status
:url url
:body parsed
:raw-body text})))
(cond
(and (map? parsed) (seq parsed))
parsed
(string/blank? text)
{}
:else
{:stdout text
:stderr ""}))))
;; -----------------------
;; Worker WS upgrade to Sprites exec
@@ -307,17 +329,27 @@
" --no-telemetry >/tmp/sandbox-agent.log 2>&1 &"))]
(sprites-exec-post! env sprite-name ["bash" "-lc" bootstrap])))
(declare sprites-exec-output)
(defn- <sprite-health! [^js env sprite-name port agent-token retries interval-ms]
(if (<= retries 0)
(throw (ex-info "sandbox-agent health check timed out in sprite"
{:sprite sprite-name :port port}))
(let [script (str "curl -fsS "
(let [script (str "if curl -fsS "
(curl-auth-arg agent-token)
" "
(sprite-local-url port "/v1/health")
" >/dev/null")]
" >/dev/null; then echo __HEALTH_OK__; else echo __HEALTH_FAIL__; fi")]
(-> (sprites-exec-post! env sprite-name ["bash" "-lc" script])
(p/then (fn [_] true))
(p/then (fn [result]
(let [{:keys [stdout stderr]} (sprites-exec-output result)
output (str stdout "\n" stderr)]
(when-not (string/includes? output "__HEALTH_OK__")
(throw (ex-info "sprite health check failed"
{:sprite sprite-name
:port port
:stdout stdout
:stderr stderr})))
true)))
(p/catch (fn [_]
(p/let [_ (p/delay interval-ms)]
(<sprite-health! env sprite-name port agent-token (dec retries) interval-ms))))))))
@@ -371,8 +403,53 @@
"default"))}))
(defn- message-payload [message]
(cond-> {:message (:message message)}
(string? (:kind message)) (assoc :kind (:kind message))))
{:message (:message message)})
(def ^:private sprite-http-status-marker "__HTTP_STATUS__")
(defn- parse-sprite-http-status [text]
(when (string? text)
(some-> (re-seq (re-pattern (str sprite-http-status-marker ":(\\d+)")) text)
last
second
(parse-int 400))))
(defn- strip-sprite-http-status [text]
(if-not (string? text)
""
(string/replace text
(re-pattern (str "(?:\\n?" sprite-http-status-marker ":\\d+)+\\s*$"))
"")))
(defn- sprites-exec-output [result]
(let [stdout (or (:stdout result)
(get-in result [:result :stdout])
(get-in result [:data :stdout])
(get-in result [:output :stdout]))
stderr (or (:stderr result)
(get-in result [:result :stderr])
(get-in result [:data :stderr])
(get-in result [:output :stderr]))
exit-code (or (:exitCode result)
(:exit-code result)
(get-in result [:result :exitCode])
(get-in result [:result :exit-code])
(get-in result [:data :exitCode])
(get-in result [:data :exit-code])
(get-in result [:output :exitCode])
(get-in result [:output :exit-code]))]
{:stdout (cond
(string? stdout) stdout
(some? stdout) (str stdout)
:else "")
:stderr (cond
(string? stderr) stderr
(some? stderr) (str stderr)
:else "")
:exit-code (cond
(number? exit-code) exit-code
(string? exit-code) (parse-int exit-code nil)
:else nil)}))
(defn- <sprite-create-session! [^js env sprite-name port agent-token session-id payload]
(let [script (str "resp=$(curl -sS -w '\\n%{http_code}' -X POST -H 'content-type: application/json' "
@@ -382,25 +459,63 @@
(curl-json-arg payload)
" "
(sprite-local-url port (str "/v1/sessions/" session-id)) "); "
"status=$(echo \"$resp\" | tail -n1); "
"http_status=$(echo \"$resp\" | tail -n1); "
"body=$(echo \"$resp\" | sed '$d'); "
"printf \"%s\" \"$body\"; "
"printf \"STATUS:%s\" \"$status\" 1>&2")]
"printf \"\\n" sprite-http-status-marker ":%s\" \"$http_status\"; ")]
(p/let [result (sprites-exec-post! env sprite-name ["bash" "-lc" script])
stdout (or (:stdout result) "")
stderr (or (:stderr result) "")
status (some-> (re-find #"STATUS:(\\d+)" stderr)
second
(parse-int 400))
parsed (parse-json-safe stdout)]
(when (and (number? status) (not (<= 200 status 299)))
{:keys [stdout stderr exit-code]} (sprites-exec-output result)
status (or (parse-sprite-http-status stderr)
(parse-sprite-http-status stdout))
parsed (parse-json-safe (strip-sprite-http-status stdout))]
(when (or
(not (number? status))
(and (number? status) (not (<= 200 status 299))))
(throw (ex-info "sandbox-agent create-session failed"
{:status status
:body parsed
:raw-body stdout
:stderr stderr})))
(println :debug :sprite-create-session :status status :exit-code exit-code)
(assoc parsed :session-id session-id))))
(defn- transient-create-session-error?
[error]
(let [{:keys [status raw-body stderr]} (ex-data error)
msg (ex-message error)
haystack (-> (str (or raw-body "") "\n" (or stderr "") "\n" (or msg ""))
string/lower-case)]
(or (= status 0)
(string/includes? haystack "__http_status__:000")
(string/includes? haystack "failed to connect")
(string/includes? haystack "could not connect")
(string/includes? haystack "connection refused")
(string/includes? haystack "connect(): connection refused")
(string/includes? haystack "connection reset")
(string/includes? haystack "timed out"))))
(defn- <sprite-create-session-with-retry!
[^js env sprite-name port agent-token session-id payload retries interval-ms]
(-> (<sprite-create-session! env sprite-name port agent-token session-id payload)
(p/catch (fn [error]
(if (and (> retries 0) (transient-create-session-error? error))
(do
(println :debug :sprite-create-session-retry
{:sprite sprite-name
:session-id session-id
:retries-left retries
:error (ex-data error)})
(p/let [_ (p/delay interval-ms)]
(<sprite-create-session-with-retry! env
sprite-name
port
agent-token
session-id
payload
(dec retries)
interval-ms)))
(p/rejected error))))))
(defn- <sprite-clone-repo! [^js env sprite-name session-id task]
(when-let [cmd (repo-clone-command env session-id task)]
(sprites-exec-post! env sprite-name ["bash" "-lc" cmd])))
@@ -684,13 +799,22 @@
port (parse-int (env-str env "SPRITES_SANDBOX_AGENT_PORT") 2468)
agent-token (env-str env "SANDBOX_AGENT_TOKEN") ;; may be nil if --no-token
health-retries (parse-int (env-str env "SPRITES_HEALTH_RETRIES") 120)
health-interval-ms (parse-int (env-str env "SPRITES_HEALTH_INTERVAL_MS") 500)]
health-interval-ms (parse-int (env-str env "SPRITES_HEALTH_INTERVAL_MS") 500)
create-session-retries (parse-int (env-str env "SPRITES_CREATE_SESSION_RETRIES") 20)
create-session-interval-ms (parse-int (env-str env "SPRITES_CREATE_SESSION_INTERVAL_MS") 250)]
(p/let [_ (sprites-create-or-get! env name)
_ (<sprite-clone-repo! env name session-id task)
_ (<sprite-bootstrap! env name port task session-id)
_ (<sprite-health! env name port agent-token health-retries health-interval-ms)
payload (session-payload task)
response (<sprite-create-session! env name port agent-token session-id payload)]
response (<sprite-create-session-with-retry! env
name
port
agent-token
session-id
payload
create-session-retries
create-session-interval-ms)]
{:provider "sprites"
:sprite-name name
:sandbox-port port
@@ -723,14 +847,18 @@
(env-str env "SANDBOX_AGENT_TOKEN"))]
(when-not (string? name)
(throw (ex-info "missing sprite-name on runtime" {:runtime runtime})))
(let [script (str "curl -fsS -X POST "
(let [payload-arg (curl-json-arg (message-payload message))
auth-arg (curl-auth-arg agent-token)
messages-url (sprite-local-url port (str "/v1/sessions/" (:session-id runtime) "/messages"))
script (str "curl -fsS -X POST "
"-H 'content-type: application/json' "
(curl-auth-arg agent-token)
auth-arg
" "
(curl-json-arg (message-payload message))
payload-arg
" "
(sprite-local-url port (str "/v1/sessions/" (:session-id runtime) "/messages"))
messages-url
" >/dev/null")]
(js/console.log "[agent:sprites-send-message]" script)
(p/let [_ (sprites-exec-post! env name ["bash" "-lc" script])]
true))))

View File

@@ -149,3 +149,136 @@
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done))))))))
(deftest init-does-not-wait-for-open-events-stream-test
(testing "session init returns immediately even when runtime events stream stays open"
(async done
(let [calls (atom {:create 0
:events-sse 0})
original-fetch js/fetch
env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"
"SANDBOX_AGENT_URL" "http://sandbox.local"}
self (make-self env)
headers {"content-type" "application/json"
"x-user-id" "user-1"}
init-body {:id "sess-init-fast"
:project {:id "project-1"}
:agent "codex"}
timeout-id (js/setTimeout (fn []
(set! js/fetch original-fetch)
(is false "init blocked waiting on events stream")
(done))
250)]
(set! js/fetch
(fn [request]
(let [url (.-url request)
method (.-method request)]
(cond
(and (= "POST" method)
(string/includes? url "/v1/sessions/sess-init-fast")
(not (string/includes? url "/messages")))
(do
(swap! calls update :create inc)
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:ok true})
#js {:status 200
:headers #js {"content-type" "application/json"}})))
(and (= "GET" method)
(string/includes? url "/v1/sessions/sess-init-fast/events/sse"))
(do
(swap! calls update :events-sse inc)
(let [stream (js/TransformStream.)]
;; Never close/read completion from this stream.
(js/Promise.resolve
(js/Response.
(.-readable stream)
#js {:status 200
:headers #js {"content-type" "text/event-stream"}}))))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:error "unhandled request"})
#js {:status 500
:headers #js {"content-type" "application/json"}}))))))
(-> (agent-do/handle-fetch self
(json-request "http://db-sync.local/__session__/init"
"POST"
init-body
headers))
(.then (fn [resp]
(js/clearTimeout timeout-id)
(set! js/fetch original-fetch)
(is (= 200 (.-status resp)))
(is (= 1 (:create @calls)))
(is (= 1 (:events-sse @calls)))
(done)))
(.catch (fn [error]
(js/clearTimeout timeout-id)
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done))))))))
(deftest init-does-not-await-start-runtime-events-stream-return-test
(testing "session init should not await start-runtime-events-stream! promise"
(async done
(let [calls (atom {:create 0})
original-fetch js/fetch
env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"
"SANDBOX_AGENT_URL" "http://sandbox.local"}
self (make-self env)
headers {"content-type" "application/json"
"x-user-id" "user-1"}
init-body {:id "sess-init-no-await"
:project {:id "project-1"}
:agent "codex"}
timeout-id (js/setTimeout (fn []
(set! js/fetch original-fetch)
(is false "init awaited start-runtime-events-stream! promise")
(done))
250)]
(set! js/fetch
(fn [request]
(let [url (.-url request)
method (.-method request)]
(cond
(and (= "POST" method)
(string/includes? url "/v1/sessions/sess-init-no-await")
(not (string/includes? url "/messages")))
(do
(swap! calls update :create inc)
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:ok true})
#js {:status 200
:headers #js {"content-type" "application/json"}})))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:error "unhandled request"})
#js {:status 500
:headers #js {"content-type" "application/json"}}))))))
(with-redefs [agent-do/start-runtime-events-stream!
(fn [& _]
(js/Promise. (fn [_resolve _reject])))]
(-> (agent-do/handle-fetch self
(json-request "http://db-sync.local/__session__/init"
"POST"
init-body
headers))
(.then (fn [resp]
(js/clearTimeout timeout-id)
(set! js/fetch original-fetch)
(is (= 200 (.-status resp)))
(is (= 1 (:create @calls)))
(done)))
(.catch (fn [error]
(js/clearTimeout timeout-id)
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done)))))))))

View File

@@ -3,6 +3,18 @@
[clojure.string :as string]
[logseq.db-sync.worker.agent.runtime-provider :as runtime-provider]))
(defn- fetch-url [request]
(cond
(string? request) request
(instance? js/URL request) (.toString request)
(some? request) (.-url request)
:else ""))
(defn- fetch-method [request init]
(or (when (some? init) (aget init "method"))
(when (and (some? request) (not (string? request))) (.-method request))
"GET"))
(deftest provider-kind-test
(testing "normalizes configured runtime provider"
(is (= "sprites" (runtime-provider/provider-kind #js {})))
@@ -100,9 +112,9 @@
task {:agent {:provider "codex"}}
original-fetch js/fetch]
(set! js/fetch
(fn [request]
(is (= "http://127.0.0.1:2468/v1/sessions/sess-1" (.-url request)))
(is (= "POST" (.-method request)))
(fn [request init]
(is (= "http://127.0.0.1:2468/v1/sessions/sess-1" (fetch-url request)))
(is (= "POST" (fetch-method request init)))
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:ok true})
@@ -128,9 +140,9 @@
:session-id "sess-2"}
original-fetch js/fetch]
(set! js/fetch
(fn [request]
(is (= "http://sandbox.local/v1/sessions/sess-2/events/sse" (.-url request)))
(is (= "GET" (.-method request)))
(fn [request init]
(is (= "http://sandbox.local/v1/sessions/sess-2/events/sse" (fetch-url request)))
(is (= "GET" (fetch-method request init)))
(js/Promise.resolve
(js/Response. "data: {\"type\":\"ok\"}\n\n"
#js {:status 200
@@ -154,9 +166,9 @@
:session-id "sess-2"}
original-fetch js/fetch]
(set! js/fetch
(fn [request]
(is (= "http://sandbox.local/v1/sessions/sess-2/messages" (.-url request)))
(is (= "POST" (.-method request)))
(fn [request init]
(is (= "http://sandbox.local/v1/sessions/sess-2/messages" (fetch-url request)))
(is (= "POST" (fetch-method request init)))
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:ok true})
@@ -172,6 +184,237 @@
(is false (str "unexpected error: " error))
(done)))))))
(deftest sprites-provider-send-message-test
(async done
(let [captured (atom nil)
env #js {"SPRITE_TOKEN" "sprite-token"}
provider (runtime-provider/create-provider env "sprites")
runtime {:provider "sprites"
:sprite-name "sprite-1"
:sandbox-port 2468
:session-id "sess-4"}
original-fetch js/fetch]
(set! js/fetch
(fn [request init]
(let [url (fetch-url request)
parsed (js/URL. url)
cmds (vec (.getAll (.-searchParams parsed) "cmd"))]
(reset! captured {:url url
:method (fetch-method request init)
:script (nth cmds 2 nil)})
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:ok true})
#js {:status 200 :headers #js {"content-type" "application/json"}})))))
(-> (runtime-provider/<send-message! provider runtime {:message "hello sprites"})
(.then (fn [ok?]
(set! js/fetch original-fetch)
(is (true? ok?))
(is (= "POST" (:method @captured)))
(is (string/includes? (:url @captured) "/v1/sprites/sprite-1/exec"))
(is (string/includes? (:script @captured) "/v1/sessions/sess-4/messages"))
(is (not (string/includes? (:script @captured) "/messages/stream")))
(done)))
(.catch (fn [error]
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done)))))))
(deftest sprites-provider-provision-parses-nested-exec-output-test
(async done
(let [env #js {"SPRITE_TOKEN" "sprite-token"}
provider (runtime-provider/create-provider env "sprites")
task {:agent {:provider "codex"}}
original-fetch js/fetch]
(set! js/fetch
(fn [request init]
(let [url (fetch-url request)
method (fetch-method request init)]
(cond
(and (= "POST" method)
(string/includes? url "/v1/sprites")
(not (string/includes? url "/exec")))
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:name "logseq-task-sess-ok"})
#js {:status 200 :headers #js {"content-type" "application/json"}}))
(and (= "POST" method)
(string/includes? url "/v1/sprites/logseq-task-sess-ok/exec"))
(let [parsed (js/URL. url)
cmds (vec (.getAll (.-searchParams parsed) "cmd"))
script (nth cmds 2 nil)
create-session? (and (string? script)
(string/includes? script "/v1/sessions/sess-ok"))]
(js/Promise.resolve
(js/Response.
(js/JSON.stringify
(clj->js (if create-session?
{:result {:stdout "{\"ok\":true}\n__HTTP_STATUS__:200__HTTP_STATUS__:200"
:stderr ""}}
{:result {:stdout ""
:stderr ""}})))
#js {:status 200 :headers #js {"content-type" "application/json"}})))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:error "unexpected request"})
#js {:status 500 :headers #js {"content-type" "application/json"}}))))))
(-> (runtime-provider/<provision-runtime! provider "sess-ok" task)
(.then (fn [runtime]
(set! js/fetch original-fetch)
(is (= "sess-ok" (:session-id runtime)))
(is (= "sprites" (:provider runtime)))
(done)))
(.catch (fn [error]
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done)))))))
(deftest sprites-provider-provision-retries-create-session-on-transient-connection-error-test
(async done
(let [calls (atom {:create-session 0})
env #js {"SPRITE_TOKEN" "sprite-token"
"SPRITES_HEALTH_RETRIES" "2"
"SPRITES_HEALTH_INTERVAL_MS" "1"}
provider (runtime-provider/create-provider env "sprites")
task {:agent {:provider "codex"}}
original-fetch js/fetch]
(set! js/fetch
(fn [request init]
(let [url (fetch-url request)
method (fetch-method request init)]
(cond
(and (= "POST" method)
(string/includes? url "/v1/sprites")
(not (string/includes? url "/exec")))
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:name "logseq-task-sess-retry"})
#js {:status 200 :headers #js {"content-type" "application/json"}}))
(and (= "POST" method)
(string/includes? url "/v1/sprites/logseq-task-sess-retry/exec"))
(let [parsed (js/URL. url)
cmds (vec (.getAll (.-searchParams parsed) "cmd"))
script (nth cmds 2 nil)
create-session? (and (string? script)
(string/includes? script "/v1/sessions/sess-retry"))
health? (and (string? script)
(string/includes? script "/v1/health"))]
(cond
create-session?
(let [n (swap! calls update :create-session inc)]
(js/Promise.resolve
(js/Response.
(js/JSON.stringify
(clj->js (if (= 1 (:create-session n))
{:result {:stdout "curl: (7) Failed to connect to 127.0.0.1 port 2468\n__HTTP_STATUS__:000"
:stderr ""}}
{:result {:stdout "{\"ok\":true}\n__HTTP_STATUS__:200"
:stderr ""}})))
#js {:status 200 :headers #js {"content-type" "application/json"}})))
health?
(js/Promise.resolve
(js/Response.
(js/JSON.stringify (clj->js {:result {:stdout "__HEALTH_OK__" :stderr ""}}))
#js {:status 200 :headers #js {"content-type" "application/json"}}))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify (clj->js {:result {:stdout "" :stderr ""}}))
#js {:status 200 :headers #js {"content-type" "application/json"}}))))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:error "unexpected request"})
#js {:status 500 :headers #js {"content-type" "application/json"}}))))))
(-> (runtime-provider/<provision-runtime! provider "sess-retry" task)
(.then (fn [runtime]
(set! js/fetch original-fetch)
(is (= "sess-retry" (:session-id runtime)))
(is (= "sprites" (:provider runtime)))
(is (= 2 (:create-session @calls)))
(done)))
(.catch (fn [error]
(set! js/fetch original-fetch)
(is false (str "unexpected error: " error))
(done)))))))
(deftest sprites-provider-provision-does-not-retry-create-session-on-http-400-test
(async done
(let [calls (atom {:create-session 0})
env #js {"SPRITE_TOKEN" "sprite-token"
"SPRITES_HEALTH_RETRIES" "2"
"SPRITES_HEALTH_INTERVAL_MS" "1"}
provider (runtime-provider/create-provider env "sprites")
task {:agent {:provider "codex"}}
original-fetch js/fetch]
(set! js/fetch
(fn [request init]
(let [url (fetch-url request)
method (fetch-method request init)]
(cond
(and (= "POST" method)
(string/includes? url "/v1/sprites")
(not (string/includes? url "/exec")))
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:name "logseq-task-sess-400"})
#js {:status 200 :headers #js {"content-type" "application/json"}}))
(and (= "POST" method)
(string/includes? url "/v1/sprites/logseq-task-sess-400/exec"))
(let [parsed (js/URL. url)
cmds (vec (.getAll (.-searchParams parsed) "cmd"))
script (nth cmds 2 nil)
create-session? (and (string? script)
(string/includes? script "/v1/sessions/sess-400"))
health? (and (string? script)
(string/includes? script "/v1/health"))]
(cond
create-session?
(do
(swap! calls update :create-session inc)
(js/Promise.resolve
(js/Response.
(js/JSON.stringify
(clj->js {:result {:stdout "{\"error\":\"bad request\"}\n__HTTP_STATUS__:400"
:stderr ""}}))
#js {:status 200 :headers #js {"content-type" "application/json"}})))
health?
(js/Promise.resolve
(js/Response.
(js/JSON.stringify (clj->js {:result {:stdout "__HEALTH_OK__" :stderr ""}}))
#js {:status 200 :headers #js {"content-type" "application/json"}}))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify (clj->js {:result {:stdout "" :stderr ""}}))
#js {:status 200 :headers #js {"content-type" "application/json"}}))))
:else
(js/Promise.resolve
(js/Response.
(js/JSON.stringify #js {:error "unexpected request"})
#js {:status 500 :headers #js {"content-type" "application/json"}}))))))
(-> (runtime-provider/<provision-runtime! provider "sess-400" task)
(.then (fn [_]
(set! js/fetch original-fetch)
(is false "expected provisioning to fail")
(done)))
(.catch (fn [error]
(set! js/fetch original-fetch)
(is (= 1 (:create-session @calls)))
(is (= 400 (:status (ex-data error))))
(done)))))))
(deftest cloudflare-provider-provision-test
(async done
(let [calls (atom {:health 0})