1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201 | (ns ird.helpers-test
"Scripted-sequence tests for the polling convenience helpers."
(:require
[cheshire.core :as json]
[clojure.test :refer [deftest is testing]]
[ird.auth :as auth]
[ird.client :as ird]
[ird.ipfs :as ipfs]
[ird.jobs :as jobs]
[ird.transport :as transport]))
(def client (ird/make-client {:api-key "ird_k"}))
(defn- scripted
"An *http-fn* answering each request with the next [status body-map] from
RESPONSES, recording requests into CAPTURED."
[captured responses]
(let [queue (atom (vec responses))]
(fn [req]
(swap! captured conj req)
(let [[status body] (or (first @queue)
(throw (ex-info "Scripted responses exhausted"
{:uri (:uri req)})))]
(swap! queue subvec 1)
{:status status :body (json/generate-string body)}))))
(defn- no-sleep [f]
(binding [transport/*sleep-fn* (fn [_])]
(f)))
(deftest job-builders-test
(testing "every job type has a builder producing its typed payload"
(is (= {:type "domain-availability-bulk" :domains ["a.ro" "b.ro"]}
(jobs/domain-availability-bulk ["a.ro" "b.ro"])))
(is (= {:type "embedding" :input "text"}
(jobs/embedding "text")))
(is (= {:type "embedding" :input "text" :model "BAAI/bge-m3"}
(jobs/embedding "text" {:model "BAAI/bge-m3"})))
(is (= {:type "llm-chat"
:messages [{:role "user" :content "hi"}]
:max_tokens 64}
(jobs/llm-chat [{:role "user" :content "hi"}] {:max_tokens 64})))
(is (= {:type "llm-raw-completion" :prompt "Once" :temperature 0.7}
(jobs/llm-raw-completion "Once" {:temperature 0.7})))
(is (= {:type "nlp-analyze" :input "text" :language "ro"}
(jobs/nlp-analyze "text" {:language "ro"})))
(is (= {:type "nlp-detect-language" :input "salut"}
(jobs/nlp-detect-language "salut")))))
(deftest multimodal-helpers-test
(testing "content parts build OpenAI-style structures"
(is (= {:type "text" :text "describe this"} (jobs/text-part "describe this")))
(let [part (jobs/image-part (.getBytes "fakepng") "image/png")]
(is (= "image_url" (:type part)))
(is (re-find #"^data:image/png;base64," (get-in part [:image_url :url]))))
(let [msg (jobs/user-message "what is here?"
(jobs/image-part (.getBytes "x") "image/webp"))]
(is (= "user" (:role msg)))
(is (= 2 (count (:content msg))))
(is (= "text" (get-in msg [:content 0 :type])))))
(testing "mime sniffing from file extension"
(is (thrown-with-msg? clojure.lang.ExceptionInfo
#"Cannot guess image MIME type"
(jobs/image-part (.getBytes "x"))))))
(deftest jobs-run-test
(testing "enqueue -> poll through processing -> fetch result"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[202 {:job {:id "j1" :status "queued"}}]
[200 {:job {:id "j1" :status "processing"}}]
[200 {:job {:id "j1" :status "succeeded"}}]
[200 {:result {:results [{:domain "a.ro" :status "available"}]}}]])]
(no-sleep
#(let [result (jobs/run client (jobs/domain-availability-bulk ["a.ro"]))]
(is (= "available" (get-in result [:result :results 0 :status])))))
(is (= ["POST /jobs" "GET /jobs/j1" "GET /jobs/j1" "GET /jobs/j1/result"]
(mapv (fn [r] (str (.toUpperCase (name (:method r))) " "
(subs (:uri r) (count "https://api.ird.ro"))))
@captured))))))
(testing "failed job throws :ird/job-failed with the job map"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[202 {:job {:id "j2" :status "queued"}}]
[200 {:job {:id "j2" :status "failed" :error "boom"}}]])]
(let [e (no-sleep
#(try (jobs/run client (jobs/embedding "x"))
(catch clojure.lang.ExceptionInfo ex ex)))]
(is (= :ird/job-failed (:type (ex-data e))))
(is (= "j2" (get-in (ex-data e) [:job :id])))))))
(testing "deadline exceeded throws :ird/timeout"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[202 {:job {:id "j3" :status "queued"}}]
[200 {:job {:id "j3" :status "processing"}}]])]
(let [e (no-sleep
#(try (jobs/run client (jobs/embedding "x") {:timeout-ms -1})
(catch clojure.lang.ExceptionInfo ex ex)))]
(is (= :ird/timeout (:type (ex-data e)))))))))
(deftest ipfs-add-file-test
(testing "grant -> raw upload to grant URL -> poll until completed"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[200 {:upload {:id "up1" :pin_id "p1"
:url "https://node1.ird.ro/up/abc"
:token "grant-tok" :status "issued"}}]
[200 {:ok true}]
[200 {:upload {:id "up1" :status "issued"}}]
[200 {:upload {:id "up1" :pin_id "p1" :status "completed"
:cid "bafy123"}}]])]
(no-sleep
#(let [upload (ipfs/add-file client (.getBytes "hello ipfs") {:name "hello.txt"})]
(is (= "bafy123" (:cid upload)))
(is (= "p1" (:pin_id upload)))))
(let [[start-req upload-req poll1 poll2] @captured]
(is (re-find #"/ipfs/uploads$" (:uri start-req)))
(is (= "hello.txt" (:name (json/parse-string (:body start-req) true))))
(is (= "https://node1.ird.ro/up/abc" (:uri upload-req)))
(is (= "Bearer grant-tok" (get-in upload-req [:headers "Authorization"])))
(is (= "application/octet-stream" (get-in upload-req [:headers "Content-Type"])))
(is (re-find #"/ipfs/uploads/up1$" (:uri poll1)))
(is (re-find #"/ipfs/uploads/up1$" (:uri poll2)))))))
(testing "failed upload throws :ird/upload-failed"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[200 {:upload {:id "up2" :url "https://n/u" :token "t" :status "issued"}}]
[200 {:ok true}]
[200 {:upload {:id "up2" :status "failed"}}]])]
(let [e (no-sleep
#(try (ipfs/add-file client (.getBytes "x"))
(catch clojure.lang.ExceptionInfo ex ex)))]
(is (= :ird/upload-failed (:type (ex-data e)))))))))
(deftest paginate-offset-test
(testing "walks pages of :limit until a short page ends it"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[200 {:pins [{:id "p1"} {:id "p2"}] :total 5
:limit 2 :offset 0}]
[200 {:pins [{:id "p3"} {:id "p4"}] :total 5
:limit 2 :offset 2}]
[200 {:pins [{:id "p5"}] :total 5
:limit 2 :offset 4}]])]
(is (= ["p1" "p2" "p3" "p4" "p5"]
(mapv :id (transport/paginate-offset
client :get "/ipfs/domains/d1/pins" :pins
{:limit 2})))))
(is (= ["/ipfs/domains/d1/pins?limit=2&offset=0"
"/ipfs/domains/d1/pins?limit=2&offset=2"
"/ipfs/domains/d1/pins?limit=2&offset=4"]
(mapv #(subs (:uri %) (count "https://api.ird.ro")) @captured)))))
(testing "an exactly-full last page needs one more (empty) request"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured
[[200 {:pins [{:id "p1"} {:id "p2"}] :total 2}]
[200 {:pins [] :total 2}]])]
(is (= 2 (count (transport/paginate-offset
client :get "/ipfs/pins" :pins {:limit 2})))))
(is (= 2 (count @captured)))))
(testing "filters ride along and the default page size is 100"
(let [captured (atom [])]
(binding [transport/*http-fn*
(scripted captured [[200 {:pins [{:id "p1"}]}]])]
(is (= 1 (count (transport/paginate-offset
client :get "/ipfs/pins" :pins {:status "pinned"})))))
(is (= "/ipfs/pins?status=pinned&limit=100&offset=0"
(subs (:uri (first @captured)) (count "https://api.ird.ro")))))))
(deftest device-login-test
(testing "start -> prompt -> poll until approved -> complete"
(let [captured (atom [])
prompted (atom nil)]
(binding [transport/*http-fn*
(scripted captured
[[200 {:device_code "dc" :user_code "AB-12"
:verification_uri "https://ird.ro/device"
:verification_uri_complete "https://ird.ro/device?code=AB-12"
:interval 1 :expires_in 600}]
[400 {:error {:code "authorization-pending"
:message "authorization pending"}}]
[429 {:error {:code "slow-down"
:message "slow down"}}]
[200 {:approved true}]
[200 {:access_token "jwt-token" :refresh_token "r" :user {:email "x@y.ro"}}]])]
(no-sleep
#(let [tokens (auth/device-login (ird/make-client {})
{:on-prompt (fn [start] (reset! prompted (:user_code start)))
:confirmation-fn (fn [_] "CONF-99")})]
(is (= "jwt-token" (:access_token tokens)))))
(is (= "AB-12" @prompted))
(let [complete-req (last @captured)]
(is (re-find #"/auth/device/complete" (:uri complete-req)))
(is (= "CONF-99" (:confirmation_code (json/parse-string (:body complete-req) true)))))))))
|