aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-11-13 13:16:44 -0600
committerCraig Jennings <c@cjennings.net>2025-11-13 13:16:44 -0600
commita064c5d7659d84393fd8f586bb1c7d18a5436b38 (patch)
tree58ce7ac137226bc6f40a2a359b138ee58cbb33db
parentc4c64e2762d7b75f588c20bc7d43ddf8ba30c97e (diff)
downloadorg-drill-a064c5d7659d84393fd8f586bb1c7d18a5436b38.tar.gz
org-drill-a064c5d7659d84393fd8f586bb1c7d18a5436b38.zip
test: Add boundary, error, and edge case tests
Added 66 comprehensive tests covering: - Entry detection with extreme values and Unicode - SM2 algorithm with boundary conditions - Workflow error handling with malformed data - Card types with complex content structures
-rw-r--r--tests/test-card-type-simple.el247
-rw-r--r--tests/test-card-type-twosided.el345
-rw-r--r--tests/test-integration-drill-session-simple-workflow-integration-test.el268
-rw-r--r--tests/test-org-drill-determine-next-interval-sm2.el136
-rw-r--r--tests/test-org-drill-entry-p.el86
5 files changed, 1082 insertions, 0 deletions
diff --git a/tests/test-card-type-simple.el b/tests/test-card-type-simple.el
index 5770883..17e4d07 100644
--- a/tests/test-card-type-simple.el
+++ b/tests/test-card-type-simple.el
@@ -209,5 +209,252 @@ Card should still be valid, answer would come from DRILL_ANSWER property."
(should (eq default-fn explicit-fn))
(should (eq default-fn 'org-drill-present-simple-card))))
+;;; Aggressive Edge Cases
+
+(ert-deftest test-card-type-simple-edge-unicode-in-question-and-answer ()
+ "Test simple card with Unicode characters in question and answer.
+Unicode should be preserved correctly."
+ (let ((content "* Unicode Card :drill:
+
+Question: What is 北京 (Beijing) in Japanese?
+
+** Answer
+
+東京 (Tokyo) is different. 北京 is Běijīng in Chinese.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-special-chars-in-content ()
+ "Test simple card with special characters (@#$%^&*) in content."
+ (let ((content "* Special Chars Card :drill:
+
+Question: What does @#$%^&*() mean?
+
+** Answer
+
+These are special characters used in programming: @#$%^&*()
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-deeply-nested-answer-subheadings ()
+ "Test simple card with very deeply nested answer structure.
+Should handle deep nesting without issues."
+ (let ((content "* Nested Answers :drill:
+
+Question text.
+
+** Level 2 Answer
+*** Level 3
+**** Level 4
+***** Level 5
+****** Level 6
+
+Deep answer content.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-answer-with-lists ()
+ "Test simple card with lists in answer.
+Org-mode lists should be preserved in answer."
+ (let ((content "* Card with List Answer :drill:
+
+Question: List three primary colors?
+
+** Answer
+
+- Red
+- Blue
+- Yellow
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-answer-with-table ()
+ "Test simple card with org-mode table in answer."
+ (let ((content "* Card with Table :drill:
+
+Question: Show conversion table.
+
+** Answer
+
+| Celsius | Fahrenheit |
+|---------+------------|
+| 0 | 32 |
+| 100 | 212 |
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-answer-with-code-block ()
+ "Test simple card with source code block in answer."
+ (let ((content "* Card with Code :drill:
+
+Question: How to print in Python?
+
+** Answer
+
+#+BEGIN_SRC python
+print(\"Hello, World!\")
+#+END_SRC
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-both-property-and-subheading-answer ()
+ "Test simple card with both DRILL_ANSWER property and answer subheading.
+One of them should take precedence (typically property)."
+ (let ((content "* Dual Answer Card :drill:
+:PROPERTIES:
+:DRILL_ANSWER: Property answer
+:END:
+
+Question text.
+
+** Answer
+
+Subheading answer.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (equal "Property answer" (org-entry-get (point) "DRILL_ANSWER")))))))
+
+(ert-deftest test-card-type-simple-edge-extremely-long-drill-answer-property ()
+ "Test simple card with very long DRILL_ANSWER property.
+Long property values should be handled correctly."
+ (let* ((long-answer (make-string 10000 ?x))
+ (content (format "* Long Answer Property Card :drill:
+:PROPERTIES:
+:DRILL_ANSWER: %s
+:END:
+
+Question.
+" long-answer)))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (let ((answer (org-entry-get (point) "DRILL_ANSWER")))
+ (should answer)
+ (should (> (length answer) 5000)))))))
+
+(ert-deftest test-card-type-simple-edge-answer-subheading-with-tags ()
+ "Test simple card where answer subheading has tags.
+Tags in subheadings should not interfere with card behavior."
+ (let ((content "* Question Card :drill:
+
+Question text.
+
+** Answer :important:note:
+
+Answer content.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-multiple-drill-answer-properties ()
+ "Test behavior with duplicate DRILL_ANSWER properties (malformed).
+Should handle gracefully, using first or last value."
+ (let ((content "* Duplicate Properties Card :drill:
+:PROPERTIES:
+:DRILL_ANSWER: First answer
+:DRILL_ANSWER: Second answer
+:END:
+
+Question.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ ;; Org-mode typically uses last value for duplicate properties
+ (let ((answer (org-entry-get (point) "DRILL_ANSWER")))
+ (should answer))))))
+
+(ert-deftest test-card-type-simple-edge-answer-with-links ()
+ "Test simple card with org-mode links in answer."
+ (let ((content "* Card with Links :drill:
+
+Question: What is Emacs?
+
+** Answer
+
+Emacs is a [[https://www.gnu.org/software/emacs/][text editor]].
+See also: [[file:manual.org][manual]].
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-question-with-formatting ()
+ "Test simple card with org-mode formatting in question.
+Bold, italic, code, etc. should be preserved."
+ (let ((content "* Formatted Question :drill:
+
+Question: What is *bold*, /italic/, =code=, and ~verbatim~?
+
+** Answer
+
+These are org-mode text formatting options.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-empty-answer-subheading ()
+ "Test simple card with empty answer subheading.
+Answer subheading exists but has no content."
+ (let ((content "* Empty Answer Card :drill:
+
+Question text.
+
+** Answer
+
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-card-type-simple-edge-answer-with-drawers ()
+ "Test simple card with drawers in answer section.
+Org-mode drawers should be preserved."
+ (let ((content "* Card with Drawer Answer :drill:
+
+Question text.
+
+** Answer
+
+:LOGBOOK:
+- Note taken on [2024-01-01]
+:END:
+
+Answer content after drawer.
+"))
+ (test-card-type-simple--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
(provide 'test-card-type-simple)
;;; test-card-type-simple.el ends here
diff --git a/tests/test-card-type-twosided.el b/tests/test-card-type-twosided.el
index cda508f..bda4f79 100644
--- a/tests/test-card-type-twosided.el
+++ b/tests/test-card-type-twosided.el
@@ -214,5 +214,350 @@ Two sides for word translation pairs."
(should (eq twosided-fn 'org-drill-present-two-sided-card))
(should (eq simple-fn 'org-drill-present-simple-card))))
+;;; Aggressive Edge Cases
+
+(ert-deftest test-card-type-twosided-edge-unicode-in-sides ()
+ "Test two-sided card with Unicode in both sides.
+Unicode characters should be preserved correctly."
+ (let ((content "* Unicode Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** English
+
+Hello
+
+** 日本語
+
+こんにちは
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-very-long-side-content ()
+ "Test two-sided card with very long content in sides.
+Should handle large amounts of text without issues."
+ (let* ((long-text (make-string 5000 ?x))
+ (content (format "* Long Sides :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Side A
+
+%s
+
+** Side B
+
+%s
+" long-text long-text)))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-lists ()
+ "Test two-sided card with org-mode lists in sides."
+ (let ((content "* Lists Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question
+
+What are primary colors?
+
+** Answer
+
+- Red
+- Blue
+- Yellow
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-tables ()
+ "Test two-sided card with org-mode tables in sides."
+ (let ((content "* Table Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question
+
+Temperature conversion?
+
+** Table
+
+| Celsius | Fahrenheit |
+|---------+------------|
+| 0 | 32 |
+| 100 | 212 |
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-code-blocks ()
+ "Test two-sided card with source code blocks in sides."
+ (let ((content "* Code Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question
+
+Python hello world?
+
+** Code
+
+#+BEGIN_SRC python
+print(\"Hello, World!\")
+#+END_SRC
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-links ()
+ "Test two-sided card with org-mode links in sides."
+ (let ((content "* Links Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Term
+
+Emacs
+
+** Definition
+
+A [[https://www.gnu.org/software/emacs/][powerful text editor]].
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-formatting ()
+ "Test two-sided card with org-mode text formatting in sides."
+ (let ((content "* Formatted Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question
+
+What is *bold*, /italic/, =code=?
+
+** Answer
+
+These are *org-mode* /text/ =formatting= options.
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-tags ()
+ "Test two-sided card where sides have tags.
+Tags on subheadings should not interfere with card."
+ (let ((content "* Card with Tagged Sides :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question :important:
+
+What is the answer?
+
+** Answer :note:
+
+42
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-nested-subheadings-in-sides ()
+ "Test two-sided card with nested subheadings within sides.
+Nested content should be part of the side."
+ (let ((content "* Nested Content :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Side A
+
+Content A
+
+*** Nested Under A
+
+More details.
+
+** Side B
+
+Content B
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-empty-headings ()
+ "Test two-sided card where heading text is minimal."
+ (let ((content "* Minimal Headings :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** A
+
+Full content for A.
+
+** B
+
+Full content for B.
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-special-characters-in-sides ()
+ "Test two-sided card with special characters in side content."
+ (let ((content "* Special Chars :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Question
+
+What are these: @#$%^&*()?
+
+** Answer
+
+Special programming characters.
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-many-extra-sides ()
+ "Test two-sided card with many subheadings (10+).
+Only first two should be used for presentation."
+ (let ((content "* Many Sides :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Side 1
+
+Content 1
+
+** Side 2
+
+Content 2
+
+** Side 3
+
+Extra
+
+** Side 4
+
+Extra
+
+** Side 5
+
+Extra
+
+** Side 6
+
+Extra
+
+** Side 7
+
+Extra
+
+** Side 8
+
+Extra
+
+** Side 9
+
+Extra
+
+** Side 10
+
+Extra
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (>= (test-card-type-twosided--count-subheadings) 10))))))
+
+(ert-deftest test-card-type-twosided-edge-no-subheadings ()
+ "Test two-sided card with no subheadings at all.
+Card is invalid but should not crash."
+ (let ((content "* No Sides :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+Just main body, no subheadings.
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 0 (test-card-type-twosided--count-subheadings)))))))
+
+(ert-deftest test-card-type-twosided-edge-sides-with-drawers ()
+ "Test two-sided card with drawers in sides.
+Drawers should be preserved."
+ (let ((content "* Drawer Card :drill:
+:PROPERTIES:
+:DRILL_CARD_TYPE: twosided
+:END:
+
+** Side A
+
+:LOGBOOK:
+- Note taken on [2024-01-01]
+:END:
+
+Content A
+
+** Side B
+
+Content B
+"))
+ (test-card-type-twosided--with-card
+ content
+ (lambda ()
+ (should (org-drill-entry-p))
+ (should (= 2 (test-card-type-twosided--count-subheadings)))))))
+
(provide 'test-card-type-twosided)
;;; test-card-type-twosided.el ends here
diff --git a/tests/test-integration-drill-session-simple-workflow-integration-test.el b/tests/test-integration-drill-session-simple-workflow-integration-test.el
index 9e839a2..7cc72c3 100644
--- a/tests/test-integration-drill-session-simple-workflow-integration-test.el
+++ b/tests/test-integration-drill-session-simple-workflow-integration-test.el
@@ -344,5 +344,273 @@ Simulates reviewing a card and verifies all components work together."
(should (= (nth 2 retrieved-data) new-failures))
(should (= (nth 3 retrieved-data) new-total)))))))))
+;;; Error Cases - Data Corruption and Malformation
+
+(ert-deftest test-integration-simple-workflow-error-invalid-property-values ()
+ "Test behavior with invalid (non-numeric) property values.
+Should handle gracefully or provide sensible defaults."
+ (let ((content "* Corrupted Card :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: invalid_number
+:DRILL_REPEATS_SINCE_FAIL: abc
+:DRILL_TOTAL_REPEATS: xyz
+:DRILL_FAILURE_COUNT: 0
+:DRILL_AVERAGE_QUALITY: not_a_float
+:DRILL_EASE: 2.5
+:END:
+
+Question content.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Corrupted Card :drill:")
+ (beginning-of-line)
+ ;; Should not error when retrieving data with invalid values
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ ;; Verify function handles corruption gracefully
+ (should (= (length data) 6)))))))
+
+(ert-deftest test-integration-simple-workflow-error-missing-properties ()
+ "Test behavior with completely missing drill properties.
+Should provide sensible defaults for new/uninitialized cards."
+ (let ((content "* Card Without Properties :drill:
+
+Question: Test question?
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Card Without Properties :drill:")
+ (beginning-of-line)
+ ;; Should not error, provide defaults
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ (should (= (length data) 6)))))))
+
+(ert-deftest test-integration-simple-workflow-error-negative-property-values ()
+ "Test behavior with negative values in properties.
+Negative values should be handled gracefully or sanitized."
+ (let ((content "* Negative Values Card :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: -5
+:DRILL_REPEATS_SINCE_FAIL: -1
+:DRILL_TOTAL_REPEATS: -10
+:DRILL_FAILURE_COUNT: -3
+:DRILL_AVERAGE_QUALITY: -2.5
+:DRILL_EASE: -1.0
+:END:
+
+Question content.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Negative Values Card :drill:")
+ (beginning-of-line)
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ ;; Verify system doesn't crash with negative values
+ (should (= (length data) 6)))))))
+
+(ert-deftest test-integration-simple-workflow-error-extremely-large-values ()
+ "Test behavior with extremely large property values.
+Should handle large numbers without overflow or crash."
+ (let ((content "* Large Values Card :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 999999999
+:DRILL_REPEATS_SINCE_FAIL: 1000000
+:DRILL_TOTAL_REPEATS: 9999999
+:DRILL_FAILURE_COUNT: 500000
+:DRILL_AVERAGE_QUALITY: 5.0
+:DRILL_EASE: 10.0
+:END:
+
+Question content.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Large Values Card :drill:")
+ (beginning-of-line)
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ (should (= (length data) 6))
+ ;; Verify scheduling can handle extreme values
+ (cl-destructuring-bind (last-interval repeats failures total-repeats meanq ease)
+ data
+ (let ((result (org-drill-determine-next-interval-sm2
+ last-interval repeats ease 4
+ failures meanq total-repeats)))
+ (should (listp result)))))))))
+
+;;; Error Cases - Workflow Error Handling
+
+(ert-deftest test-integration-simple-workflow-error-retrieve-data-not-at-heading ()
+ "Test retrieving data when point is not at a heading.
+Should handle gracefully without crashing."
+ (test-integration-simple-workflow--with-drill-buffer
+ test-integration-simple-workflow-basic-entries
+ (lambda ()
+ ;; Move to body text, not a heading
+ (re-search-forward "Question: What is 2\\+2\\?")
+ (beginning-of-line)
+ ;; Should not crash, may return nil or defaults
+ (let ((data (org-drill-get-item-data)))
+ ;; Just verify it doesn't crash
+ (should (or (null data) (listp data)))))))
+
+(ert-deftest test-integration-simple-workflow-error-retrieve-data-at-non-drill-heading ()
+ "Test retrieving data from heading without drill tag.
+Should handle non-drill headings gracefully."
+ (test-integration-simple-workflow--with-drill-buffer
+ test-integration-simple-workflow-basic-entries
+ (lambda ()
+ ;; Find non-drill heading
+ (re-search-forward "^\\* Not a drill card")
+ (beginning-of-line)
+ (should-not (org-drill-entry-p))
+ ;; Retrieving data from non-drill entry
+ (let ((data (org-drill-get-item-data)))
+ ;; Should not crash
+ (should (or (null data) (listp data)))))))
+
+(ert-deftest test-integration-simple-workflow-error-nested-drill-entries ()
+ "Test handling of nested drill entries (parent and child both tagged).
+Should detect both as separate drill entries."
+ (let ((content "* Parent Drill Entry :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 5
+:DRILL_REPEATS_SINCE_FAIL: 2
+:DRILL_TOTAL_REPEATS: 3
+:DRILL_FAILURE_COUNT: 0
+:DRILL_AVERAGE_QUALITY: 4.0
+:DRILL_EASE: 2.5
+:END:
+
+Parent question.
+
+** Child Drill Entry :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 3
+:DRILL_REPEATS_SINCE_FAIL: 1
+:DRILL_TOTAL_REPEATS: 2
+:DRILL_FAILURE_COUNT: 1
+:DRILL_AVERAGE_QUALITY: 3.5
+:DRILL_EASE: 2.3
+:END:
+
+Child question.
+"))
+ (let ((count (test-integration-simple-workflow--count-drill-entries content)))
+ ;; Should find both parent and child as separate entries
+ (should (= count 2)))))
+
+(ert-deftest test-integration-simple-workflow-error-very-deep-nesting ()
+ "Test drill entries at very deep nesting levels.
+Should handle deep heading structures without issues."
+ (let ((content "* Level 1
+** Level 2
+*** Level 3
+**** Level 4
+***** Level 5
+****** Level 6 :drill:
+
+Deep question.
+"))
+ (let ((count (test-integration-simple-workflow--count-drill-entries content)))
+ (should (= count 1)))))
+
+;;; Error Cases - Robustness
+
+(ert-deftest test-integration-simple-workflow-error-unicode-in-properties ()
+ "Test handling of Unicode characters in drill properties.
+Should handle or reject Unicode in numeric properties gracefully."
+ (let ((content "* Unicode Property Card :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 5
+:DRILL_REPEATS_SINCE_FAIL: 2
+:DRILL_TOTAL_REPEATS: 3
+:DRILL_FAILURE_COUNT: 0
+:DRILL_AVERAGE_QUALITY: 4.0
+:DRILL_EASE: 2.5
+:END:
+
+Question with Unicode: 日本語 Café.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Unicode Property Card :drill:")
+ (beginning-of-line)
+ ;; Should handle Unicode in content without issues
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ (should (= (length data) 6)))))))
+
+(ert-deftest test-integration-simple-workflow-error-special-chars-in-heading ()
+ "Test drill entries with special characters in heading.
+Special chars should not interfere with entry detection or data handling."
+ (let ((content "* Card @#$%^&*() Special :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 5
+:DRILL_REPEATS_SINCE_FAIL: 2
+:DRILL_TOTAL_REPEATS: 3
+:DRILL_FAILURE_COUNT: 0
+:DRILL_AVERAGE_QUALITY: 4.0
+:DRILL_EASE: 2.5
+:END:
+
+Question.
+"))
+ (let ((count (test-integration-simple-workflow--count-drill-entries content)))
+ (should (= count 1)))))
+
+(ert-deftest test-integration-simple-workflow-error-empty-properties-drawer ()
+ "Test drill entry with empty PROPERTIES drawer.
+Should handle empty drawer gracefully."
+ (let ((content "* Empty Drawer Card :drill:
+:PROPERTIES:
+:END:
+
+Question.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Empty Drawer Card :drill:")
+ (beginning-of-line)
+ (let ((data (org-drill-get-item-data)))
+ ;; Should provide defaults for missing properties
+ (should (listp data))
+ (should (= (length data) 6)))))))
+
+(ert-deftest test-integration-simple-workflow-error-float-in-integer-property ()
+ "Test behavior with float values in expected-integer properties.
+System should handle type coercion or rounding."
+ (let ((content "* Float Values Card :drill:
+:PROPERTIES:
+:DRILL_LAST_INTERVAL: 5.7
+:DRILL_REPEATS_SINCE_FAIL: 2.3
+:DRILL_TOTAL_REPEATS: 3.9
+:DRILL_FAILURE_COUNT: 0.5
+:DRILL_AVERAGE_QUALITY: 4.0
+:DRILL_EASE: 2.5
+:END:
+
+Question.
+"))
+ (test-integration-simple-workflow--with-drill-buffer
+ content
+ (lambda ()
+ (re-search-forward "^\\* Float Values Card :drill:")
+ (beginning-of-line)
+ (let ((data (org-drill-get-item-data)))
+ (should (listp data))
+ ;; Verify system handles float->int conversion
+ (should (numberp (nth 0 data)))
+ (should (numberp (nth 1 data))))))))
+
(provide 'test-integration-drill-session-simple-workflow-integration-test)
;;; test-integration-drill-session-simple-workflow-integration-test.el ends here
diff --git a/tests/test-org-drill-determine-next-interval-sm2.el b/tests/test-org-drill-determine-next-interval-sm2.el
index abb7a0b..a895ccd 100644
--- a/tests/test-org-drill-determine-next-interval-sm2.el
+++ b/tests/test-org-drill-determine-next-interval-sm2.el
@@ -303,5 +303,141 @@ Third review interval should be significantly larger than second."
(interval-3rd (test-sm2--extract-interval result-3rd)))
(should (> interval-3rd (* interval-2nd 1.5)))))) ; Significant growth
+;;; Aggressive Boundary Cases
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-repeat-count ()
+ "Test SM2 with very high repeat count (1000 reviews).
+Algorithm should remain stable with extreme n values."
+ (let ((org-drill-add-random-noise-to-intervals-p nil))
+ (let* ((result (org-drill-determine-next-interval-sm2 365 1000 2.5 4 0 4.0 1000))
+ (interval (test-sm2--extract-interval result))
+ (repeats (test-sm2--extract-repeats result))
+ (ef (test-sm2--extract-ef result)))
+ (should (numberp interval))
+ (should (> interval 0))
+ (should (= repeats 1001))
+ (should (>= ef test-sm2-min-ef)))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-long-interval ()
+ "Test SM2 with very long last interval (10 years = 3650 days).
+Algorithm should handle extreme intervals without overflow."
+ (let ((org-drill-add-random-noise-to-intervals-p nil))
+ (let* ((result (org-drill-determine-next-interval-sm2 3650 100 2.5 4 0 4.0 100))
+ (interval (test-sm2--extract-interval result)))
+ (should (numberp interval))
+ (should (> interval 0)))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-failure-count ()
+ "Test SM2 with very high failure count (100+ failures).
+Failure count should be tracked correctly even at extreme values."
+ (let* ((failures 100)
+ (result (org-drill-determine-next-interval-sm2 10 3 2.5 0 failures 3.0 150))
+ (new-failures (test-sm2--extract-failures result)))
+ (should (= new-failures (1+ failures)))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-very-high-total-repeats ()
+ "Test SM2 with very high total-repeats (10000+).
+Total repeats should increment correctly even at extreme values."
+ (let* ((total-repeats 10000)
+ (result (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 4.0 total-repeats))
+ (new-total (test-sm2--extract-total-repeats result)))
+ (should (= new-total (1+ total-repeats)))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-ef-just-above-floor ()
+ "Test SM2 with EF just above minimum floor (1.31).
+EF should be maintained or modified normally, not clamped."
+ (let* ((result (org-drill-determine-next-interval-sm2 10 3 1.31 4 0 4.0 10))
+ (ef (test-sm2--extract-ef result)))
+ (should (>= ef test-sm2-min-ef))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-ef-at-floor ()
+ "Test SM2 with EF exactly at minimum floor (1.3).
+EF at floor with quality 4 should increase slightly."
+ (let* ((result (org-drill-determine-next-interval-sm2 10 3 1.3 4 0 4.0 10))
+ (ef (test-sm2--extract-ef result)))
+ (should (>= ef test-sm2-min-ef))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-perfect-recall-after-many-failures ()
+ "Test perfect recall (quality 5) after long failure streak.
+Perfect recall should not reset failures, but should calculate normal interval."
+ (let ((org-drill-add-random-noise-to-intervals-p nil))
+ (let* ((failures 50)
+ (result (org-drill-determine-next-interval-sm2 10 3 2.5 5 failures 3.0 100))
+ (interval (test-sm2--extract-interval result))
+ (new-failures (test-sm2--extract-failures result)))
+ (should (> interval 0))
+ (should (= new-failures failures))))) ; Failures unchanged on success
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-failure-after-long-streak ()
+ "Test complete failure (quality 0) after long successful streak.
+Failure should reset interval to -1 regardless of previous success."
+ (let* ((result (org-drill-determine-next-interval-sm2 365 100 2.8 0 0 4.5 100))
+ (interval (test-sm2--extract-interval result))
+ (repeats (test-sm2--extract-repeats result))
+ (failures (test-sm2--extract-failures result)))
+ (should (= interval -1))
+ (should (= repeats 1)) ; Reset to 1
+ (should (= failures 1))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-meanq-at-maximum ()
+ "Test SM2 with meanq at maximum (5.0).
+High quality review with already-perfect meanq."
+ (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 5 0 5.0 100))
+ (meanq (test-sm2--extract-meanq result)))
+ (should (<= meanq 5.0))
+ (should (>= meanq 4.5)))) ; Should remain very high
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-meanq-at-minimum ()
+ "Test SM2 with meanq at minimum (0.0).
+Low quality history with poor current recall."
+ (let* ((result (org-drill-determine-next-interval-sm2 10 3 2.5 3 5 0.0 100))
+ (meanq (test-sm2--extract-meanq result)))
+ (should (>= meanq 0.0))
+ (should (<= meanq 1.0)))) ; Should remain low with quality 3
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-high-total-repeats-low-meanq ()
+ "Test SM2 with very high total-repeats but low meanq.
+Indicates item reviewed many times but poorly recalled."
+ (let* ((total-repeats 1000)
+ (meanq 2.0)
+ (result (org-drill-determine-next-interval-sm2 5 10 1.5 3 20 meanq total-repeats))
+ (new-meanq (test-sm2--extract-meanq result)))
+ (should (numberp new-meanq))
+ ;; meanq should remain low given poor history
+ (should (< new-meanq 3.0))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-zero-last-interval ()
+ "Test SM2 with zero last-interval.
+Should handle as first review (n=1 case)."
+ (let* ((result (org-drill-determine-next-interval-sm2 0 1 2.5 4 0 4.0 0))
+ (interval (test-sm2--extract-interval result)))
+ (should (= interval 1))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-float-vs-int-intervals ()
+ "Test SM2 with float last-interval vs integer.
+Algorithm should handle both correctly."
+ (let ((org-drill-add-random-noise-to-intervals-p nil))
+ (let* ((result-float (org-drill-determine-next-interval-sm2 10.5 3 2.5 4 0 4.0 10))
+ (result-int (org-drill-determine-next-interval-sm2 10 3 2.5 4 0 4.0 10))
+ (interval-float (test-sm2--extract-interval result-float))
+ (interval-int (test-sm2--extract-interval result-int)))
+ (should (numberp interval-float))
+ (should (numberp interval-int))
+ ;; Both should produce reasonable intervals
+ (should (> interval-float 10))
+ (should (> interval-int 10)))))
+
+(ert-deftest test-org-drill-determine-next-interval-sm2-boundary-alternating-quality-pattern ()
+ "Test SM2 behavior with alternating quality (5, 0, 5, 0 pattern).
+Simulates inconsistent learning."
+ (let* ((result-1 (org-drill-determine-next-interval-sm2 1 2 2.5 5 0 5.0 1))
+ (ef-1 (test-sm2--extract-ef result-1))
+ (result-2 (org-drill-determine-next-interval-sm2 6 3 ef-1 0 0 4.5 2))
+ (interval-2 (test-sm2--extract-interval result-2))
+ (failures-2 (test-sm2--extract-failures result-2)))
+ ;; After perfect then fail, should reset interval
+ (should (= interval-2 -1))
+ (should (= failures-2 1))))
+
(provide 'test-org-drill-determine-next-interval-sm2)
;;; test-org-drill-determine-next-interval-sm2.el ends here
diff --git a/tests/test-org-drill-entry-p.el b/tests/test-org-drill-entry-p.el
index c298c94..4af9169 100644
--- a/tests/test-org-drill-entry-p.el
+++ b/tests/test-org-drill-entry-p.el
@@ -178,5 +178,91 @@ Single asterisk should still work as heading."
(lambda ()
(should (org-drill-entry-p)))))
+;;; Aggressive Boundary Cases
+
+(ert-deftest test-org-drill-entry-p-boundary-very-long-tag-name ()
+ "Test drill tag with very long name (edge case).
+Tag name length should not cause issues."
+ (let* ((long-tag (make-string 200 ?d))
+ (content (format "* Heading :%s:\n\nContent\n" long-tag)))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ ;; Should not match 'drill' tag
+ (should-not (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-many-tags ()
+ "Test heading with many tags including drill.
+Should still correctly identify drill tag among many others."
+ (let ((content "* Heading :tag1:tag2:tag3:drill:tag4:tag5:tag6:tag7:tag8:tag9:tag10:\n\nContent\n"))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-unicode-in-heading ()
+ "Test heading with unicode characters and drill tag.
+Unicode should not interfere with tag detection."
+ (let ((content "* Café München 北京 :drill:\n\nUnicode content 日本語\n"))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-deep-nesting ()
+ "Test drill tag detection at deep nesting level.
+Should work at any heading level."
+ (let ((content "* Level 1\n** Level 2\n*** Level 3\n**** Level 4\n***** Level 5 :drill:\n\nDeep content\n"))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ ;; Navigate to deeply nested heading
+ (re-search-forward "Level 5")
+ (beginning-of-line)
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-tag-with-numbers ()
+ "Test that tags containing 'drill' substring but with numbers don't match.
+drill123 should not be recognized as drill tag."
+ (let ((content "* Heading :drill123:\n\nContent\n"))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ (should-not (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-drill-at-tag-boundary ()
+ "Test drill tag at beginning and end of tag list."
+ (let ((content1 "* Heading :drill:other:tags:\n\nContent\n")
+ (content2 "* Heading :other:tags:drill:\n\nContent\n"))
+ ;; drill at start
+ (test-org-drill-entry-p--with-org-buffer
+ content1
+ (lambda ()
+ (should (org-drill-entry-p))))
+ ;; drill at end
+ (test-org-drill-entry-p--with-org-buffer
+ content2
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-special-chars-in-heading ()
+ "Test heading with special characters and drill tag.
+Special chars should not break tag parsing."
+ (let ((content "* Heading with @#$%^&*() special chars :drill:\n\nContent\n"))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
+(ert-deftest test-org-drill-entry-p-boundary-very-long-heading-line ()
+ "Test heading with very long title text.
+Long heading should not affect tag detection."
+ (let* ((long-title (make-string 1000 ?x))
+ (content (format "* %s :drill:\n\nContent\n" long-title)))
+ (test-org-drill-entry-p--with-org-buffer
+ content
+ (lambda ()
+ (should (org-drill-entry-p))))))
+
(provide 'test-org-drill-entry-p)
;;; test-org-drill-entry-p.el ends here