|
197 | 197 | :role :system}]} |
198 | 198 | (h/messages))))))) |
199 | 199 |
|
| 200 | +(defn ^:private prompt-with-title! |
| 201 | + "Like prompt! but accepts a :sync-prompt-mock for title generation testing. |
| 202 | + Accepts optional :config in mocks to override default config." |
| 203 | + [params mocks] |
| 204 | + (let [api-mock (fn [{:keys [on-first-response-received on-message-received]}] |
| 205 | + (on-first-response-received {:type :text :text "response"}) |
| 206 | + (on-message-received {:type :text :text "response"}) |
| 207 | + (on-message-received {:type :finish})) |
| 208 | + {:keys [chat-id] :as resp} |
| 209 | + (with-redefs [llm-api/sync-or-async-prompt! api-mock |
| 210 | + llm-api/sync-prompt! (:sync-prompt-mock mocks) |
| 211 | + f.tools/call-tool! (constantly nil) |
| 212 | + f.tools/all-tools (constantly []) |
| 213 | + f.tools/approval (constantly :allow) |
| 214 | + config/await-plugins-resolved! (constantly true)] |
| 215 | + (h/config! (merge {:env "test" :chat {:title true}} |
| 216 | + (:config mocks))) |
| 217 | + (swap! (h/db*) update :models |
| 218 | + (fn [models] |
| 219 | + (merge {"openai/gpt-5.2" {:tools true}} |
| 220 | + (or models {})))) |
| 221 | + (f.chat/prompt params (h/db*) (h/messenger) (h/config) (h/metrics)))] |
| 222 | + (is (match? {:chat-id string? :status :prompting} resp)) |
| 223 | + {:chat-id chat-id})) |
| 224 | + |
| 225 | +(deftest title-generation-test |
| 226 | + (testing "generates title on first message and re-generates at third with full context" |
| 227 | + (h/reset-components!) |
| 228 | + (let [sync-prompt-calls* (atom []) |
| 229 | + call-count* (atom 0) |
| 230 | + sync-mock (fn [params] |
| 231 | + (swap! sync-prompt-calls* conj params) |
| 232 | + {:output-text (str "Title " (swap! call-count* inc))})] |
| 233 | + ;; Message 1: should generate title |
| 234 | + (let [{:keys [chat-id]} (prompt-with-title! {:message "Help me debug"} {:sync-prompt-mock sync-mock})] |
| 235 | + (is (= 1 (count @sync-prompt-calls*)) |
| 236 | + "Should call sync-prompt! on first message") |
| 237 | + (is (= "Title 1" (get-in (h/db) [:chats chat-id :title]))) |
| 238 | + (is (= 1 (count (:user-messages (first @sync-prompt-calls*)))) |
| 239 | + "First title uses only the current user message") |
| 240 | + |
| 241 | + ;; Message 2: should NOT re-generate title |
| 242 | + (h/reset-messenger!) |
| 243 | + (prompt-with-title! {:message "Also refactor" :chat-id chat-id} {:sync-prompt-mock sync-mock}) |
| 244 | + (is (= 1 (count @sync-prompt-calls*)) |
| 245 | + "Should NOT call sync-prompt! on second message") |
| 246 | + (is (= "Title 1" (get-in (h/db) [:chats chat-id :title]))) |
| 247 | + |
| 248 | + ;; Message 3: should re-generate title with full conversation context |
| 249 | + (h/reset-messenger!) |
| 250 | + (prompt-with-title! {:message "And add tests" :chat-id chat-id} {:sync-prompt-mock sync-mock}) |
| 251 | + (is (= 2 (count @sync-prompt-calls*)) |
| 252 | + "Should call sync-prompt! again on third message") |
| 253 | + (is (= "Title 2" (get-in (h/db) [:chats chat-id :title]))) |
| 254 | + (let [retitle-messages (:user-messages (second @sync-prompt-calls*))] |
| 255 | + (is (> (count retitle-messages) 1) |
| 256 | + "Third message title should include full conversation history")) |
| 257 | + |
| 258 | + ;; Message 4: should NOT re-generate title |
| 259 | + (h/reset-messenger!) |
| 260 | + (prompt-with-title! {:message "One more thing" :chat-id chat-id} {:sync-prompt-mock sync-mock}) |
| 261 | + (is (= 2 (count @sync-prompt-calls*)) |
| 262 | + "Should NOT call sync-prompt! after third message")))) |
| 263 | + |
| 264 | + (testing "manual rename suppresses automatic re-titling" |
| 265 | + (h/reset-components!) |
| 266 | + (let [sync-prompt-calls* (atom []) |
| 267 | + sync-mock (fn [params] |
| 268 | + (swap! sync-prompt-calls* conj params) |
| 269 | + {:output-text "Auto Title"})] |
| 270 | + ;; Message 1: generates initial title |
| 271 | + (let [{:keys [chat-id]} (prompt-with-title! {:message "Help me debug"} {:sync-prompt-mock sync-mock})] |
| 272 | + (is (= 1 (count @sync-prompt-calls*))) |
| 273 | + |
| 274 | + ;; Manual rename |
| 275 | + (f.chat/update-chat {:chat-id chat-id :title "My Custom Title"} (h/db*) (h/messenger) (h/metrics)) |
| 276 | + (is (= "My Custom Title" (get-in (h/db) [:chats chat-id :title]))) |
| 277 | + (is (true? (get-in (h/db) [:chats chat-id :title-custom?]))) |
| 278 | + |
| 279 | + ;; Messages 2 and 3: should NOT re-generate because of manual rename |
| 280 | + (h/reset-messenger!) |
| 281 | + (prompt-with-title! {:message "Second msg" :chat-id chat-id} {:sync-prompt-mock sync-mock}) |
| 282 | + (h/reset-messenger!) |
| 283 | + (prompt-with-title! {:message "Third msg" :chat-id chat-id} {:sync-prompt-mock sync-mock}) |
| 284 | + (is (= 1 (count @sync-prompt-calls*)) |
| 285 | + "Should NOT re-title after manual rename") |
| 286 | + (is (= "My Custom Title" (get-in (h/db) [:chats chat-id :title])))))) |
| 287 | + |
| 288 | + (testing "title disabled in config skips all generation" |
| 289 | + (h/reset-components!) |
| 290 | + (let [sync-prompt-calls* (atom []) |
| 291 | + sync-mock (fn [params] |
| 292 | + (swap! sync-prompt-calls* conj params) |
| 293 | + {:output-text "Should not appear"}) |
| 294 | + disabled-mocks {:sync-prompt-mock sync-mock :config {:chat {:title false}}}] |
| 295 | + (let [{:keys [chat-id]} (prompt-with-title! {:message "Hello"} disabled-mocks)] |
| 296 | + (prompt-with-title! {:message "Second" :chat-id chat-id} disabled-mocks) |
| 297 | + (prompt-with-title! {:message "Third" :chat-id chat-id} disabled-mocks) |
| 298 | + (is (zero? (count @sync-prompt-calls*)) |
| 299 | + "Should never call sync-prompt! when title is disabled") |
| 300 | + (is (nil? (get-in (h/db) [:chats chat-id :title]))))))) |
| 301 | + |
200 | 302 | (deftest context-overflow-auto-compact-guard-test |
201 | 303 | (testing "context overflow after auto-compact reports error instead of looping" |
202 | 304 | (h/reset-components!) |
|
0 commit comments