1
0
Fork 0

Compare commits

...

54 commits

Author SHA1 Message Date
Bram Schoenmakers aafd0bf27c
Merge branch 'test' 2024-02-25 14:55:02 +01:00
Bram Schoenmakers bad0c2a50e
Add docstring 2024-02-23 22:58:36 +01:00
Bram Schoenmakers eb846635a0
Avoid repetition of the choices of the summary choices 2024-02-23 22:23:11 +01:00
Bram Schoenmakers fa402763fa
Fix tests for inserting a summary 2024-02-22 23:29:46 +01:00
Bram Schoenmakers 602b7dffb4
Add tests for kagi-summarize-url 2024-02-22 23:18:44 +01:00
Bram Schoenmakers 02be283f3a
Add test for kagi-summarize-region 2024-02-22 22:30:57 +01:00
Bram Schoenmakers 199cc478df
Adapt description 2024-02-22 22:18:48 +01:00
Bram Schoenmakers b3191b750d
Allow optional pattern for which tests to run 2024-02-22 22:15:46 +01:00
Bram Schoenmakers c8a7658bd3
Create macro to check values in a request object 2024-02-22 21:56:02 +01:00
Bram Schoenmakers fbb4ead05c
Create macro to check expectations on arguments of called functions 2024-02-22 21:41:50 +01:00
Bram Schoenmakers 1702fadaeb
Adapt docstring 2024-02-22 07:05:01 +01:00
Bram Schoenmakers e2b81ef432
Add tests for kagi-summarize-buffer 2024-02-22 07:04:26 +01:00
Bram Schoenmakers 28793401df
Allow kagi-summarize-buffer to be used non-interactively 2024-02-21 12:44:31 +01:00
Bram Schoenmakers f086fd433a
Add first tests for kagi-summarize-buffer 2024-02-20 06:57:01 +01:00
Bram Schoenmakers 3324bfe1d7
Remove redundant return values
nil is the implied return value
2024-02-20 06:47:49 +01:00
Bram Schoenmakers 146d128cd2
let* to let 2024-02-18 22:28:38 +01:00
Bram Schoenmakers e3e18dfc25
Minor rephrase 2024-02-18 22:18:29 +01:00
Bram Schoenmakers f5cc1475bb
Refactored summarizer summary type selection 2024-02-18 22:17:21 +01:00
Bram Schoenmakers 01a761a2ff
Refactored summarizer engine selection and added tests 2024-02-18 20:19:44 +01:00
Bram Schoenmakers 7d6f6f1cf1
Refactored language selection
Fallback to English if all else fails (invalid language given and an
invalid default language code).
2024-02-18 09:08:54 +01:00
Bram Schoenmakers 016e453238
Reformat and add some comments 2024-02-18 08:03:01 +01:00
Bram Schoenmakers 6d1b4cbc0f
Docstring update 2024-02-18 07:47:34 +01:00
Bram Schoenmakers d2187e309a
Change test for upcasing 2024-02-18 07:47:14 +01:00
Bram Schoenmakers 21c9fcda38
Add tests on summary language, rename variable 2024-02-18 07:32:46 +01:00
Bram Schoenmakers f68276593e
Add test for number of API calls 2024-02-18 03:55:43 +01:00
Bram Schoenmakers f78c2688a4
Add some basic tests for the summarizer 2024-02-16 22:28:45 +01:00
Bram Schoenmakers de0ab986ae
Catch any API call 2024-02-16 08:00:20 +01:00
Bram Schoenmakers da103bf236
Add more interactivity tests 2024-02-16 07:48:10 +01:00
Bram Schoenmakers 76ae8b858a
Rename suite 2024-02-15 22:29:23 +01:00
Bram Schoenmakers 316df52e09
Fill comments 2024-02-15 08:01:49 +01:00
Bram Schoenmakers 8e68af9216
Check that the input text appears in the kagi-fastgpt-prompt call 2024-02-15 07:59:10 +01:00
Bram Schoenmakers 1bdbb69000
No need to have a non-nil return value 2024-02-15 07:38:53 +01:00
Bram Schoenmakers f8b01e4225
Removed obsolete tests 2024-02-14 21:14:38 +01:00
Bram Schoenmakers a83540fc0b
Add some tests for kagi-{translate,proofread} when called interactively 2024-02-14 21:11:25 +01:00
Bram Schoenmakers ed6df4ba19
Added kagi-proofread tests, adjusted kagi-translate tests 2024-02-14 20:54:32 +01:00
Bram Schoenmakers 5415473f67
(require 'buttercup) 2024-02-14 20:53:44 +01:00
Bram Schoenmakers 0c82995ed9
Added a section on setting up unit test execution 2024-02-14 20:26:15 +01:00
Bram Schoenmakers 019391a618
Merge branch 'main' into test 2024-02-14 08:49:57 +01:00
Bram Schoenmakers fbdeec45d9
Pass interactivity to kagi-fastgpt-prompt 2024-02-13 23:08:59 +01:00
Bram Schoenmakers 2dcf2b7c6a
No need to call through 2024-02-13 23:08:46 +01:00
Bram Schoenmakers a3d9eb9a30
Add some tests and define some spies at higher level 2024-02-13 22:19:09 +01:00
Bram Schoenmakers 4bb7a1d63e
Added tests for kagi-translate 2024-02-13 08:40:09 +01:00
Bram Schoenmakers ec63275484
Add more tests 2024-02-12 22:43:26 +01:00
Bram Schoenmakers 62fe326f2c
Remove obsolete variable 2024-02-12 22:43:14 +01:00
Bram Schoenmakers 4e3163f980
Cleanup the calling code
Have a single entry point to the Kagi API, which can be stubbed. Also
remove the stubbed responses as we'll cover things with testing now.
2024-02-12 12:05:39 +01:00
Bram Schoenmakers b6c7d4b6ed
Fix copyright 2024-02-12 12:03:57 +01:00
Bram Schoenmakers 4dd622c865
Merge branch 'main' into test 2024-02-12 11:22:53 +01:00
Bram Schoenmakers e6da7af9ac
Added more tests on output formatting 2024-02-12 11:21:42 +01:00
Bram Schoenmakers d59232e9bb
Enable lisp-data mode for Cask 2024-02-12 07:58:14 +01:00
Bram Schoenmakers 239b8cd17d
Update creation date 2024-02-11 20:56:16 +01:00
Bram Schoenmakers c01425f31c
First test added 2024-02-11 20:53:29 +01:00
Bram Schoenmakers 166730181a
Merge branch 'main' into test 2024-02-11 20:53:05 +01:00
Bram Schoenmakers 2a89340c1e
Compile before test 2024-02-11 20:50:42 +01:00
Bram Schoenmakers 09f24ccaa3
Add some infrastructure for testing 2024-02-11 12:12:27 +01:00
6 changed files with 550 additions and 74 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/.cask
*.elc

10
Cask Normal file
View file

@ -0,0 +1,10 @@
;;; -*- lisp-data -*-
(source gnu)
(source melpa-stable)
(source melpa)
(depends-on "shell-maker")
(development
(depends-on "buttercup"))

View file

@ -200,6 +200,17 @@ If you recognize this confusion, you may want to add the following line to your
Because the =fastgpt-shell-mode-map= only becomes available when =kagi-fastgpt-shell= has been invoked, the keybinding is done in a mode hook.
* Development
kagi.el comes with some unit tests, written with [[https://github.com/jorgenschaefer/emacs-buttercup/][buttercup]] and can be executed in a controlled [[https://github.com/cask/cask/][Cask]] environment:
1. =git clone https://github.com/cask/cask/=
2. =make -C cask install=
3. Run =cask= in the kagi.el directory to setup the environment.
4. Run the tests with =cask exec buttercup -L .=
There's also a [[https://github.com/casey/just][justfile]] which allows you to execute =just test= to compile the Emacs Lisp source and run the unit tests afterwards in one go.
* Changelog
** 0.4pre
@ -214,6 +225,10 @@ Because the =fastgpt-shell-mode-map= only becomes available when =kagi-fastgpt-s
- =kagi-proofread= asks FastGPT to proofread the region, a buffer or a text input.
- =kagi-summarize-buffer= returns the summary when called non-interactively.
- Unit tests added.
*** Fixes
- Change the prompt for =kagi-translate= to return translations only, without preamble.

14
justfile Normal file
View file

@ -0,0 +1,14 @@
set positional-arguments
# for convenience, run cask through bash
cask *args:
cask $@
compile:
cask emacs -batch -L . -L test --eval "(setq byte-compile-error-on-warn t)" -f batch-byte-compile $(cask files); (ret=$? ; cask clean-elc && exit $ret)
test pattern=".": compile
cask exec buttercup -L . --pattern {{pattern}} --no-skip
buttercup:
emacs -batch -f package-initialize -L . -f buttercup-run-discover

430
kagi-test.el Normal file
View file

@ -0,0 +1,430 @@
;;; kagi-test.el --- Kagi API tests -*- lexical-binding: t; -*-
;; Copyright (C) 2023 - 2024 Bram Schoenmakers
;; Author: Bram Schoenmakers <me@bramschoenmakers.nl>
;; Maintainer: Bram Schoenmakers <me@bramschoenmakers.nl>
;; Created: 11 Feb 2024
;; Package-Version: 0.1
;; Package-Requires: ((emacs "28.2") (shell-maker "0.44.1"))
;; Keywords: convenience
;; URL: https://codeberg.org/bram85/kagi.el
;; This file is not part of GNU Emacs.
;; MIT License
;; Copyright (c) 2023 - 2024 Bram Schoenmakers
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in all
;; copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;; Commentary:
;; API documentation: https://help.kagi.com/kagi/api/fastgpt.html
;;; Code:
(require 'buttercup)
(require 'kagi)
(defun kagi-test--dummy-output (text &optional references)
"Construct a fictitious result from the Kagi FastGPT API.
TEXT is the output text, optionally with a list of REFERENCES."
(json-encode (list (cons "data" (append
(list (cons "output" text))
(when references
(list (cons "references" references))))))))
(buttercup-define-matcher-for-binary-function
:to-be-equal-including-properties equal-including-properties
:expect-match-phrase "Expected `%A' to be equal (incl. properties) to %b, but `%A' was %a."
:expect-mismatch-phrase "Expected `%A' not to be equal (incl. properties) to %b, but `%A' was %a.")
(defmacro kagi-test--expect-arg (function-symbol num &rest expect-args)
"Check the argument NUM of the first call of FUNCTION-SYMBOL.
The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
`(let ((args (spy-calls-args-for ,function-symbol 0)))
(expect (nth ,num args) ,@expect-args)))
(defmacro kagi-test--expect-object (function-symbol key &rest expect-args)
"Check the argument NUM of the first call of FUNCTION-SYMBOL.
The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
`(let ((args (car (spy-calls-args-for ,function-symbol 0))))
(expect (map-elt args ,key) ,@expect-args)))
(describe "kagi.el"
:var ((dummy-output "text"))
(before-each
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output dummy-output)))
(describe "FastGPT"
(describe "kagi-fastgpt-prompt"
(before-each
(spy-on #'message)
(spy-on #'kagi--fastgpt-display-result))
(it "converts *bold* markup to a bold face"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "**bold**"))
(expect (kagi-fastgpt-prompt "foo")
:to-be-equal-including-properties
(propertize "bold" 'font-lock-face 'kagi-bold)))
(it "converts <b>bold</b> markup to a bold face"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "<b>bold</b>"))
(expect (kagi-fastgpt-prompt "foo")
:to-be-equal-including-properties
(propertize "bold" 'font-lock-face 'kagi-bold)))
(it "converts $italic$ markup to an italic face"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "$italic$"))
(expect (kagi-fastgpt-prompt "foo")
:to-be-equal-including-properties
(propertize "italic" 'font-lock-face 'kagi-italic)))
(it "converts ```code``` markup to a code face"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "```echo $*```"))
(expect (kagi-fastgpt-prompt "foo")
:to-be-equal-including-properties
(propertize "echo $*" 'font-lock-face 'kagi-code)))
(it "formats references properly"
(spy-on #'kagi--call-api
:and-return-value
(kagi-test--dummy-output
"Main text"
'(((title . "First title")
(snippet . "**Snippet 1**")
(url . "https://www.example.org"))
((title . "Second title")
(snippet . "Snippet $2$")
(url . "https://www.example.com")))))
(expect (kagi-fastgpt-prompt "foo")
:to-be-equal-including-properties
(format "Main text
%s First title
%s
https://www.example.org
%s Second title
Snippet %s
https://www.example.com"
(propertize "[1]" 'font-lock-face 'kagi-bold)
(propertize "Snippet 1" 'font-lock-face 'kagi-bold)
(propertize "[2]" 'font-lock-face 'kagi-bold)
(propertize "2" 'font-lock-face 'kagi-italic))))
(it "inserts the output when requested"
(spy-on #'insert)
(kagi-fastgpt-prompt "foo" t)
;; one additional insert call is to fill the temporary buffer
;; for POST data
(expect #'insert :to-have-been-called-times 2)
(expect #'insert :to-have-been-called-with dummy-output))
(it "does not insert the output by default"
(spy-on #'insert)
(kagi-fastgpt-prompt "foo")
;; one insert call is to fill the temporary buffer for POST
;; data
(expect #'insert :to-have-been-called-times 1))
(it "shows short output in the echo area when called interactively"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output dummy-output))
(kagi-fastgpt-prompt "foo" nil t)
(expect #'message :to-have-been-called-with dummy-output)
(expect #'kagi--fastgpt-display-result :not :to-have-been-called))
(it "shows longer output in a separate buffer when called interactively"
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output (format "%s\n%s" dummy-output dummy-output)))
(kagi-fastgpt-prompt "foo" nil t)
(expect #'message :not :to-have-been-called)
(expect #'kagi--fastgpt-display-result :to-have-been-called))
(it "makes exactly one API call"
(kagi-fastgpt-prompt "foo")
(expect #'kagi--call-api :to-have-been-called-times 1)))
(describe "kagi-translate"
(before-each
(spy-on #'kagi-fastgpt-prompt))
(it "calls kagi-fastgpt-prompt non-interactively with target language in prompt"
(kagi-translate "hello" "toki pona")
(expect #'kagi-fastgpt-prompt :to-have-been-called-times 1)
;; not going to test the exact phrasing of the prompt, but
;; at least 'toki pona' has to appear.
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
;; called non-interactively
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal nil))
(it "calls kagi-fastgpt-prompt non-interactively with source and target language in prompt"
(kagi-translate "bonjour" "toki pona" "French")
;; has 'toki pona' in the prompt
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
;; and has French in the prompt
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "French")
;; called non-interactively
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal nil))
(it "passes the region text to kagi-fastgpt-prompt, if active"
(spy-on #'use-region-p :and-return-value t)
(spy-on #'buffer-substring-no-properties :and-return-value "region text")
(spy-on #'region-beginning)
(spy-on #'region-end)
(spy-on #'kagi--read-language :and-return-value "toki pona")
(call-interactively #'kagi-translate)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "region text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "passes the user input if the region is inactive"
(spy-on #'use-region-p)
(spy-on #'kagi--read-language :and-return-value "toki pona")
(spy-on #'read-buffer :and-return-value "user text")
(spy-on #'get-buffer)
(call-interactively #'kagi-translate)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "user text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "passes the buffer text if buffer is selected"
(spy-on #'use-region-p)
(spy-on #'kagi--read-language :and-return-value "toki pona")
(spy-on #'read-buffer)
(spy-on #'get-buffer :and-return-value t)
(spy-on #'set-buffer)
(spy-on #'buffer-string :and-return-value "buffer text")
(call-interactively #'kagi-translate)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "buffer text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "raises an error when no text is given"
(spy-on #'use-region-p)
(spy-on #'kagi--read-language :and-return-value "toki pona")
(spy-on #'read-buffer :and-return-value "")
(spy-on #'get-buffer)
(expect (call-interactively #'kagi-translate) :to-throw)))
(describe "kagi-proofread"
(before-each
(spy-on #'kagi-fastgpt-prompt))
(it "calls kagi-fastgpt-prompt"
(kagi-proofread "foo")
(expect #'kagi-fastgpt-prompt :to-have-been-called)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "foo"))
(it "passes the region text to kagi-fastgpt-prompt, if active"
(spy-on #'use-region-p :and-return-value t)
(spy-on #'buffer-substring-no-properties :and-return-value "region text")
(spy-on #'region-beginning)
(spy-on #'region-end)
(call-interactively #'kagi-proofread)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "region text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "passes the user input if the region is inactive"
(spy-on #'use-region-p)
(spy-on #'read-buffer :and-return-value "user text")
(spy-on #'get-buffer)
(call-interactively #'kagi-proofread)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "user text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "passes the buffer text if buffer is selected"
(spy-on #'use-region-p)
(spy-on #'read-buffer)
(spy-on #'get-buffer :and-return-value t)
(spy-on #'set-buffer)
(spy-on #'buffer-string :and-return-value "buffer text")
(call-interactively #'kagi-proofread)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "buffer text")
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
(it "raises an error when no text is given"
(spy-on #'use-region-p)
(spy-on #'read-buffer :and-return-value "")
(spy-on #'get-buffer)
(expect (call-interactively #'kagi-proofread) :to-throw))))
(describe "Summarizer"
:var ((just-enough-text-input nil)
(just-too-little-text-input nil)
(dummy-https-url "https://www.example.com")
(dummy-http-url "http://www.example.com")
(dummy-ftp-url "ftp://example.com")
;; make pattern matching case sensitive
(case-fold-search nil))
(before-all
(dotimes (_ 50) (push "a" just-enough-text-input))
(setq just-too-little-text-input (string-join (cdr just-enough-text-input) " "))
(setq just-enough-text-input (string-join just-enough-text-input " ")))
(before-each
(spy-on #'kagi--call-summarizer :and-call-through)
(spy-on #'kagi--display-summary))
(describe "kagi-summarize"
:var ((kagi-summarizer-default-language))
(before-each
(setq kagi-summarizer-default-language "NL"))
(it "returns a summary on minimal text input"
(expect (kagi-summarize just-enough-text-input) :to-equal dummy-output))
(it "makes exactly one API call"
(kagi-summarize just-enough-text-input)
(expect #'kagi--call-api :to-have-been-called-times 1))
(it "throws on just too little text output"
(expect (kagi-summarize just-too-little-text-input) :to-throw))
(it "throws an error on too little text input"
(expect (kagi-summarize "foo") :to-throw))
(it "throws an error on empty input"
(expect (kagi-summarize "") :to-throw))
(it "throws an error on missing input"
(expect (kagi-summarize nil) :to-throw))
(it "returns a summary for a valid language code"
(expect (kagi-summarize just-enough-text-input "NL" :to-equal dummy-output))
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "returns a summary for a valid language code with wrong capitalization"
(expect (kagi-summarize just-enough-text-input "nL" :to-equal dummy-output))
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "returns a summary for a valid language name"
(expect (kagi-summarize just-enough-text-input "Dutch" :to-equal dummy-output))
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "returns a summary for a valid language name with different capitalization"
(expect (kagi-summarize just-enough-text-input "dUtch" :to-equal dummy-output))
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "falls back to the default language for invalid language codes"
(expect (kagi-summarize just-enough-text-input "VL") :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "falls back to the default language for invalid language names"
(expect (kagi-summarize just-enough-text-input "Valyrian") :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
(it "falls back to English if the default language is invalid"
(setq kagi-summarizer-default-language "XY")
(expect (kagi-summarize just-enough-text-input "Valyrian") :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "EN"))
(it "returns a summary for an HTTPS URL"
(expect (kagi-summarize dummy-https-url) :to-equal dummy-output))
(it "returns a summary for an uppercase HTTPS URL"
(expect (kagi-summarize (upcase dummy-https-url)) :to-equal dummy-output))
(it "returns a summary for an HTTP URL"
(expect (kagi-summarize dummy-http-url) :to-equal dummy-output))
(it "throws for an unsupported URL scheme"
(expect (kagi-summarize dummy-ftp-url) :to-throw))
(it "returns a summary for a valid engine with different capitalization"
(expect (kagi-summarize dummy-https-url nil "Daphne") :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "daphne"))
(it "uses kagi-summarizer-engine variable for invalid engine values"
(setq kagi-summarizer-engine "Daphne")
(expect (kagi-summarize dummy-https-url nil "bram") :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "daphne"))
(it "uses the cecil engine when an invalid engine is configured"
(setq kagi-summarizer-engine "steve")
(expect (kagi-summarize dummy-https-url) :to-equal dummy-output)
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "cecil"))
(it "returns a summary when the summary style is requested"
(expect (kagi-summarize just-enough-text-input nil nil 'summary) :to-equal dummy-output))
(it "returns a summary when the take-away style is requested"
(expect (kagi-summarize just-enough-text-input nil nil 'takeaway) :to-equal dummy-output))
(it "uses the summary style when an invalid format is given"
(kagi-summarize just-enough-text-input nil nil 'invalid)
(kagi-test--expect-object #'kagi--call-summarizer "summary_type" :to-equal 'summary))
(it "uses the summary style when an invalid format is configured"
(setq kagi-summarizer-default-summary-format 'invalid)
(kagi-summarize just-enough-text-input)
(kagi-test--expect-object #'kagi--call-summarizer "summary_type" :to-equal 'summary)))
(describe "kagi-summarize-buffer"
(before-each
(spy-on #'read-buffer)
(spy-on #'set-buffer)
(spy-on #'buffer-name :and-return-value "dummy-buffer")
(spy-on #'kagi-summarize :and-return-value dummy-output)
(spy-on #'kagi--insert-summary))
(it "returns the summary when called non-interactively"
(expect (kagi-summarize-buffer "dummy") :to-be dummy-output)
(expect #'kagi-summarize :to-have-been-called)
(expect #'kagi--display-summary :not :to-have-been-called)
(expect #'kagi--insert-summary :not :to-have-been-called))
(it "inserts the summary when requested, non-interactively"
(kagi-summarize-buffer "dummy" t)
(expect #'kagi-summarize :to-have-been-called)
(expect #'kagi--display-summary :not :to-have-been-called)
(expect #'kagi--insert-summary :to-have-been-called))
(it "displays the summary when called interactively"
(call-interactively #'kagi-summarize-buffer)
(expect #'kagi-summarize :to-have-been-called)
(expect #'kagi--display-summary :to-have-been-called)
(expect #'kagi--insert-summary :not :to-have-been-called))
(it "inserts the summary when requested, interactively"
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(t nil nil))
(call-interactively #'kagi-summarize-buffer)
(expect #'kagi-summarize :to-have-been-called)
(expect #'kagi--display-summary :not :to-have-been-called)
(expect #'kagi--insert-summary :to-have-been-called))
(it "passes arguments to kagi-summary"
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(t lang bram random))
(call-interactively #'kagi-summarize-buffer)
(expect #'kagi-summarize :to-have-been-called)
(expect #'kagi--display-summary :not :to-have-been-called)
(expect #'kagi--insert-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random)))
(describe "kagi-summarize-region"
(before-each
(spy-on #'region-beginning)
(spy-on #'region-end)
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(lang bram random))
(spy-on #'kagi-summarize :and-return-value dummy-output)
(spy-on #'buffer-name :and-return-value "buffer-name")
(spy-on #'buffer-substring-no-properties))
(it "passes arguments to kagi-summary"
(call-interactively #'kagi-summarize-region)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "buffer-name (summary)")
(expect #'kagi-summarize :to-have-been-called)
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random))
(it "opens a buffer with the summary"
(call-interactively #'kagi-summarize-region)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 0 :to-equal dummy-output)))
(describe "kagi-summarize-url"
(before-each
(spy-on #'kagi-summarize :and-return-value dummy-output)
(spy-on #'read-string :and-return-value "https://www.example.com")
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(nil lang bram random))
(spy-on #'kagi--insert-summary))
(it "passes arguments to kagi-summary"
(call-interactively #'kagi-summarize-url)
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random))
(it "opens a buffer with the summary"
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(expect #'kagi--insert-summary :not :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 0 :to-equal dummy-output))
(it "opens a buffer with the domain name for an HTTPS URL"
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
(it "opens a buffer with the domain name for an HTTP URL"
(spy-on #'read-string :and-return-value "http://www.example.com")
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
(it "opens a buffer with the domain name for an HTTPS URL ending with /"
(spy-on #'read-string :and-return-value "https://www.example.com/")
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
(it "opens a buffer with the domain name for an HTTPS URL ending with path"
(spy-on #'read-string :and-return-value "https://www.example.com/aaa/")
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
(it "opens a buffer with the domain name for an HTTPS URL with subdomain"
(spy-on #'read-string :and-return-value "https://abc.example.com/")
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "abc.example.com (summary)"))
(it "inserts the summary when requested non-interactively"
(kagi-summarize-url "https://www.example.com" t)
(expect #'kagi--display-summary :not :to-have-been-called)
(expect #'kagi--insert-summary :to-have-been-called)
(kagi-test--expect-arg #'kagi--insert-summary 0 :to-equal dummy-output)))))
;;; kagi-test.el ends here

154
kagi.el
View file

@ -63,14 +63,6 @@ https://kagi.com/settings?p=api"
:type '(choice string function)
:group 'kagi)
(defcustom kagi-stubbed-responses nil
"Whether the package should return a stubbed response.
To be used for testing purposes, such that no credits are spent
on dummy data."
:type 'boolean
:group 'kagi)
(defcustom kagi-fastgpt-api-url "https://kagi.com/api/v0/fastgpt"
"The Kagi FastGPT API entry point."
:type '(choice string function)
@ -257,34 +249,21 @@ https://help.kagi.com/kagi/api/fastgpt.html for more information."
"--header" "Content-Type: application/json"
"--data" "@-")))
(defvar kagi--fastgpt-stubbed-response
"{\"data\":{\"output\":\"<b>Test</b> response in **bold** and $italic$.\"}}"
"Stubbed response for the Kagi FastGPT endpoint.")
(defun kagi--call-api (object url)
"Submit the OBJECT to the API end-point at URL.
(defvar kagi--summarizer-stubbed-response
"{\"data\":{\"output\":\"```Test``` response.\"}}"
"Stubbed response for the Kagi Summarizer endpoint.")
(defun kagi--call-process-region (&rest args)
"`call-process-region' wrapper.
Calls `call-process-region' with ARGS, unless
`kagi-stubbed-responses' is non-nil.
In that case, this function will not do an actual API call but
return some dummy data."
(if (not kagi-stubbed-responses)
(apply #'call-process-region args)
(let* ((url (car (last args)))
(response (cond
((string= url kagi-fastgpt-api-url)
kagi--fastgpt-stubbed-response)
((string= url kagi-summarizer-api-url)
kagi--summarizer-stubbed-response)
(t ""))))
(erase-buffer)
(insert response)
0)))
The OBJECT will be JSON encoded and sent as HTTP POST data."
(with-temp-buffer
(insert (json-encode object))
(let* ((call-process-flags '(nil nil "curl" t t nil))
(curl-flags (kagi--curl-flags))
(all-flags (append call-process-flags
curl-flags
(list url)))
(return (apply #'call-process-region all-flags)))
(if (zerop return)
(buffer-string)
(error "Call to Kagi API returned with status %s" return)))))
(defun kagi--call-fastgpt (prompt)
"Submit the given PROMPT to the FastGPT API.
@ -292,17 +271,7 @@ return some dummy data."
Returns the JSON response as a string. See
https://help.kagi.com/kagi/api/fastgpt.html for the
interpretation."
(with-temp-buffer
(insert (json-encode `((query . ,prompt))))
(let* ((call-process-flags '(nil nil "curl" t t nil))
(curl-flags (kagi--curl-flags))
(all-flags (append call-process-flags
curl-flags
(list kagi-fastgpt-api-url)))
(return (apply #'kagi--call-process-region all-flags)))
(if (zerop return)
(buffer-string)
(error "Call to FastGPT API returned with status %s" return)))))
(kagi--call-api (list (cons 'query prompt)) kagi-fastgpt-api-url))
(defun kagi--call-summarizer (obj)
"Submit a request to the Summarizer API.
@ -312,17 +281,7 @@ The given OBJ is encoded to JSON and used as the request's POST data.
Returns the JSON response as a string. See
https://help.kagi.com/kagi/api/summarizer.html for the
interpretation."
(with-temp-buffer
(insert (json-encode obj))
(let* ((call-process-flags '(nil nil "curl" t t nil))
(curl-flags (kagi--curl-flags))
(all-flags (append call-process-flags
curl-flags
(list kagi-summarizer-api-url)))
(return (apply #'kagi--call-process-region all-flags)))
(if (zerop return)
(buffer-string)
(error "Call to Summarizer API returned with status %s" return)))))
(kagi--call-api obj kagi-summarizer-api-url))
(defun kagi--build-summarizer-request-object (items)
"Build a request object for a summary.
@ -557,6 +516,53 @@ no issues."
"Non-nil if string S is a URL."
(string-match-p (rx (seq bos "http" (? "s") "://" (+ (not space)) eos)) s))
(defun kagi--valid-language-name-p (language)
"Return non-nil if LANGUAGE is a valid language name."
(and (stringp language)
(map-elt kagi--summarizer-languages (capitalize language))))
(defun kagi--valid-language-code-p (language)
"Return t if LANGUAGE is a valid two letter language code for the summarizer."
(and (stringp language)
(seq-contains-p
(map-values kagi--summarizer-languages)
(upcase language))))
(defun kagi--summarizer-determine-language (hint)
"Determine the language for the summary given a language HINT.
The HINT may be a language code (e.g. `DE') or a language
name (e.g. `German'). If as invalid hint is given, it falls back
to `kagi-summarizer-default-language'."
(cond
((kagi--valid-language-code-p hint) (upcase hint))
((kagi--valid-language-name-p hint)
(map-elt kagi--summarizer-languages (capitalize hint)))
((kagi--valid-language-code-p kagi-summarizer-default-language)
kagi-summarizer-default-language)
(t "EN")))
(defun kagi--valid-engine-p (engine)
"Return non-nil when the given ENGINE is valid."
(and (stringp engine)
(map-elt kagi--summarizer-engines (downcase engine))))
(defun kagi--summarizer-engine (hint)
"Return a valid engine name based on the name given in HINT."
(cond ((kagi--valid-engine-p hint) (downcase hint))
((kagi--valid-engine-p kagi-summarizer-engine)
(downcase kagi-summarizer-engine))
(t "cecil")))
(defun kagi--summarizer-format (hint)
"Return a valid summary type based on the type given in HINT."
(let* ((custom-type (cdr (get 'kagi-summarizer-default-summary-format 'custom-type)))
(choices (mapcar (lambda (e) (car (last e))) custom-type)))
(cond ((seq-contains-p choices hint) hint)
((seq-contains-p choices kagi-summarizer-default-summary-format)
kagi-summarizer-default-summary-format)
(t 'summary))))
(defun kagi-summarize (text-or-url &optional language engine format)
"Return the summary of the given TEXT-OR-URL.
@ -569,15 +575,12 @@ defined in `kagi--summarizer-engines'.
FORMAT is the summary format, where `summary' returns a paragraph
of text and `takeaway' returns a bullet list."
(let* ((kagi-summarizer-default-language
(if (stringp language)
(upcase language)
kagi-summarizer-default-language))
(kagi-summarizer-engine
(if (stringp engine)
(downcase engine)
kagi-summarizer-engine))
(kagi-summarizer-default-summary-format format))
(let ((kagi-summarizer-default-language
(kagi--summarizer-determine-language language))
(kagi-summarizer-engine
(kagi--summarizer-engine engine))
(kagi-summarizer-default-summary-format
(kagi--summarizer-format format)))
(if-let* ((response (if (kagi--url-p text-or-url)
(kagi--call-url-summarizer text-or-url)
(kagi--call-text-summarizer text-or-url)))
@ -632,7 +635,7 @@ this when PROMPT-INSERT-P is non-nil."
#'string=))))))
;;;###autoload
(defun kagi-summarize-buffer (buffer &optional insert language engine format)
(defun kagi-summarize-buffer (buffer &optional insert language engine format interactive-p)
"Summarize the BUFFER's content and show it in a new window.
By default, the summary is shown in a new buffer.
@ -654,17 +657,20 @@ of text and `takeaway' returns a bullet list.
With a single universal prefix argument (`C-u'), the user is
prompted whether the summary has to be inserted at point, which
target LANGUAGE to use, which summarizer ENGINE to use and which
summary FORMAT to use."
(interactive (cons
(read-buffer (format-prompt "Buffer" "") nil t)
(kagi--get-summarizer-parameters t)))
summary FORMAT to use.
INTERACTIVE-P is t when called interactively."
(interactive (append
(list (read-buffer (format-prompt "Buffer" "") nil t))
(kagi--get-summarizer-parameters t)
(list t)))
(let ((summary (with-current-buffer buffer
(kagi-summarize (buffer-string) language engine format)))
(summary-buffer-name (with-current-buffer buffer
(kagi--summary-buffer-name (buffer-name)))))
(if (and insert (not buffer-read-only))
(kagi--insert-summary summary)
(kagi--display-summary summary summary-buffer-name))))
(cond ((and insert (not buffer-read-only)) (kagi--insert-summary summary))
(interactive-p (kagi--display-summary summary summary-buffer-name))
(t summary))))
;;;###autoload
(defun kagi-summarize-region (begin end &optional language engine format)