aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-11 14:25:55 -0500
committerCraig Jennings <c@cjennings.net>2026-06-11 14:25:55 -0500
commit8d790f371e54a8cc3e79a5ce72cd4dd5b3fa4513 (patch)
treeff6c1a496c4e7727bd823979a582dc21ef25b811 /.ai/scripts/tests
parentbdc9a5d6e1320032770f54c747c210e4f465c399 (diff)
downloadrulesets-8d790f371e54a8cc3e79a5ce72cd4dd5b3fa4513.tar.gz
rulesets-8d790f371e54a8cc3e79a5ce72cd4dd5b3fa4513.zip
feat(org): table standard as a rule, reflow helper, and lint check
Wide org tables overflow the page in exported PDF/docx, and hand-wrapping a cell into continuation rows is tedious and error-prone. The standard existed only as a work-project convention with nothing enforcing it. claude-rules/org-tables.md carries the generalized standard: 120-column budget measured at render width (a link counts as its visible label and is never split), over-budget cells wrap onto continuation rows, and a rule sits under the header and every logical row. wrap-org-table.el reflows a table to that shape mechanically. Columns shrink from natural width toward a floor of their widest atomic token, cells wrap link-safe, and rule-delimited continuation groups merge back into their logical row before re-wrapping, which makes the reflow idempotent. A table whose floors still exceed the budget reflows best-effort and stays flagged for restructuring. lint-org.el gains an org-table-standard judgment check: width overruns and missing rules surface during the sweep with a pointer to the helper. Conformant wrapped tables don't false-flag, since the check reuses the helper's continuation-group reading. The check is judgment-only by design: reflowing is a visible layout change the sweep shouldn't make silently.
Diffstat (limited to '.ai/scripts/tests')
-rw-r--r--.ai/scripts/tests/test-lint-org.el42
-rw-r--r--.ai/scripts/tests/test-wrap-org-table.el188
2 files changed, 230 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test-lint-org.el b/.ai/scripts/tests/test-lint-org.el
index 60deb8c..d4b3ba0 100644
--- a/.ai/scripts/tests/test-lint-org.el
+++ b/.ai/scripts/tests/test-lint-org.el
@@ -599,5 +599,47 @@ followups file on the next run."
(lo-test--drop-buffer file)
(when (file-exists-p file) (delete-file file)))))
+;;; ---------------------------------------------------------------------------
+;;; org-table-standard check (width budget + rules between rows)
+
+(ert-deftest lo-table-over-budget-emits-judgment ()
+ "A table line rendering wider than 120 surfaces as an org-table-standard judgment."
+ (let* ((wide (make-string 130 ?x))
+ (run (lo-test--run (format "* H\n\n| a |\n|---|\n| %s |\n|---|\n" wide)))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should (memq 'org-table-standard (lo-test--checkers judgments)))
+ (should (cl-some (lambda (i) (lo-test--has (plist-get i :msg) "120"))
+ judgments))))
+
+(ert-deftest lo-table-compliant-not-flagged ()
+ "A narrow table with rules under header and every row passes silently."
+ (let* ((run (lo-test--run "* H\n\n| a |\n|---|\n| ok |\n|---|\n"))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+
+(ert-deftest lo-table-missing-interrow-rules-emits-judgment ()
+ "Data rows without a rule between them violate the standard."
+ (let* ((run (lo-test--run "* H\n\n| a | b |\n|---+---|\n| 1 | 2 |\n| 3 | 4 |\n"))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should (memq 'org-table-standard (lo-test--checkers judgments)))
+ (should (cl-some (lambda (i) (lo-test--has (plist-get i :msg) "rule"))
+ judgments))))
+
+(ert-deftest lo-table-wide-link-source-measures-render-width ()
+ "A long link target doesn't trip the budget — width is render-measured."
+ (let* ((target (concat "https://example.com/" (make-string 120 ?p)))
+ (run (lo-test--run
+ (format "* H\n\n| [[%s][ok]] |\n|---|\n| x |\n|---|\n" target)))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+
+(ert-deftest lo-table-conformant-wrapped-table-not-flagged ()
+ "Continuation rows inside a rule-delimited group are one logical row, not a
+missing-rules violation."
+ (let* ((run (lo-test--run
+ "* H\n\n| Name | Notes |\n|-------+-------------------|\n| alpha | wrapped text that |\n| | continues here |\n|-------+-------------------|\n"))
+ (judgments (lo-test--judgments (plist-get run :issues))))
+ (should-not (memq 'org-table-standard (lo-test--checkers judgments)))))
+
(provide 'test-lint-org)
;;; test-lint-org.el ends here
diff --git a/.ai/scripts/tests/test-wrap-org-table.el b/.ai/scripts/tests/test-wrap-org-table.el
new file mode 100644
index 0000000..8d1ecb6
--- /dev/null
+++ b/.ai/scripts/tests/test-wrap-org-table.el
@@ -0,0 +1,188 @@
+;;; test-wrap-org-table.el --- ERT tests for wrap-org-table.el -*- lexical-binding: t; -*-
+;;
+;; Run from the repo root:
+;; emacs --batch -q -L .ai/scripts -l ert \
+;; -l .ai/scripts/tests/test-wrap-org-table.el \
+;; -f ert-run-tests-batch-and-exit
+;;
+;; Covers the pure core (render width, tokenizing, cell wrap, column
+;; allocation), the table reformat (exact-output cases at small budgets), and
+;; the file layer.
+
+(require 'ert)
+(require 'cl-lib)
+
+(defconst wot-test--dir
+ (file-name-directory (or load-file-name buffer-file-name default-directory))
+ "Directory of this test file, captured at load time.")
+
+(add-to-list 'load-path (expand-file-name ".." wot-test--dir))
+(require 'wrap-org-table)
+
+;;; ---------------------------------------------------------------------------
+;;; render width — links measure at their visible label
+
+(ert-deftest wot-render-width-plain-text ()
+ (should (= (wot-render-width "hello") 5)))
+
+(ert-deftest wot-render-width-empty ()
+ (should (= (wot-render-width "") 0)))
+
+(ert-deftest wot-render-width-descriptive-link-counts-label-only ()
+ (should (= (wot-render-width "[[file:long-target-path.org][label]]") 5)))
+
+(ert-deftest wot-render-width-bare-link-counts-target ()
+ (should (= (wot-render-width "[[file:x.org]]") 10)))
+
+(ert-deftest wot-render-width-mixed-text-and-link ()
+ ;; "see doc now" = 11
+ (should (= (wot-render-width "see [[file:a.org][doc]] now") 11)))
+
+;;; ---------------------------------------------------------------------------
+;;; tokenize — links are atomic tokens
+
+(ert-deftest wot-tokenize-plain-words ()
+ (should (equal (wot-tokenize "a quick fox") '("a" "quick" "fox"))))
+
+(ert-deftest wot-tokenize-link-is-one-token ()
+ (should (equal (wot-tokenize "see [[file:a.org][the doc]] now")
+ '("see" "[[file:a.org][the doc]]" "now"))))
+
+(ert-deftest wot-tokenize-empty-string ()
+ (should (equal (wot-tokenize "") nil)))
+
+;;; ---------------------------------------------------------------------------
+;;; cell wrap
+
+(ert-deftest wot-wrap-cell-fits-on-one-line ()
+ (should (equal (wot-wrap-cell "short text" 20) '("short text"))))
+
+(ert-deftest wot-wrap-cell-wraps-at-word-boundary ()
+ (should (equal (wot-wrap-cell "The quick brown fox jumps over the lazy dog" 28)
+ '("The quick brown fox jumps" "over the lazy dog"))))
+
+(ert-deftest wot-wrap-cell-empty-cell ()
+ (should (equal (wot-wrap-cell "" 10) '(""))))
+
+(ert-deftest wot-wrap-cell-overlong-token-sits-alone-unsplit ()
+ (should (equal (wot-wrap-cell "tiny incomprehensibilities end" 10)
+ '("tiny" "incomprehensibilities" "end"))))
+
+(ert-deftest wot-wrap-cell-link-never-split ()
+ ;; label is 17 chars, wider than the 10 budget — the link stays whole
+ (should (equal (wot-wrap-cell "[[file:a.org][a very wide label x]]" 10)
+ '("[[file:a.org][a very wide label x]]"))))
+
+;;; ---------------------------------------------------------------------------
+;;; column allocation
+
+(ert-deftest wot-allocate-widths-fits-naturally ()
+ ;; cells total 1+1, overhead 3*2+1=7 → 9 ≤ 30: keep natural widths
+ (should (equal (wot-allocate-widths '(("A" "B") ("1" "2")) 30) '(1 1))))
+
+(ert-deftest wot-allocate-widths-shrinks-widest-column ()
+ ;; naturals 5 + 44, overhead 7 → 56 > 40; Notes shrinks to 28, floor 5
+ (should (equal (wot-allocate-widths
+ '(("Name" "Notes")
+ ("alpha" "The quick brown fox jumps over the lazy dog"))
+ 40)
+ '(5 28))))
+
+(ert-deftest wot-allocate-widths-respects-token-floor ()
+ ;; the long token (21) floors column 2 even when the budget wants less
+ (should (equal (wot-allocate-widths
+ '(("A" "B") ("x" "incomprehensibilities yes"))
+ 20)
+ '(1 21))))
+
+;;; ---------------------------------------------------------------------------
+;;; table reformat — exact output
+
+(defconst wot-test--wide-input
+ "| Name | Notes |
+|------+-------|
+| alpha | The quick brown fox jumps over the lazy dog |
+")
+
+(defconst wot-test--wide-expected
+ "| Name | Notes |
+|-------+------------------------------|
+| alpha | The quick brown fox jumps |
+| | over the lazy dog |
+|-------+------------------------------|
+")
+
+(ert-deftest wot-reformat-wraps-wide-cell-with-continuation-rows ()
+ (should (equal (wot-reformat-table-string wot-test--wide-input 40)
+ wot-test--wide-expected)))
+
+(ert-deftest wot-reformat-narrow-table-gains-rules-between-rows ()
+ (should (equal (wot-reformat-table-string
+ "| A | B |
+|---+---|
+| 1 | 2 |
+| 3 | 4 |
+" 120)
+ "| A | B |
+|---+---|
+| 1 | 2 |
+|---+---|
+| 3 | 4 |
+|---+---|
+")))
+
+(ert-deftest wot-reformat-is-idempotent ()
+ (let ((once (wot-reformat-table-string wot-test--wide-input 40)))
+ (should (equal (wot-reformat-table-string once 40) once))))
+
+(ert-deftest wot-reformat-preserves-link-source-verbatim ()
+ (let ((out (wot-reformat-table-string
+ "| Doc | Notes |
+|-----+-------|
+| [[file:very-long-target-path.org][d1]] | ok |
+" 120)))
+ (should (string-match-p
+ (regexp-quote "[[file:very-long-target-path.org][d1]]") out))))
+
+(ert-deftest wot-reformat-headerless-table-rules-every-row ()
+ (should (equal (wot-reformat-table-string
+ "| 1 | 2 |
+| 3 | 4 |
+" 120)
+ "| 1 | 2 |
+|---+---|
+| 3 | 4 |
+|---+---|
+")))
+
+(ert-deftest wot-reformat-preserves-indentation ()
+ (should (equal (wot-reformat-table-string
+ " | A | B |
+ |---+---|
+ | 1 | 2 |
+" 120)
+ " | A | B |
+ |---+---|
+ | 1 | 2 |
+ |---+---|
+")))
+
+;;; ---------------------------------------------------------------------------
+;;; file layer
+
+(ert-deftest wot-process-file-reformats-tables-in-place ()
+ (let ((file (make-temp-file "wot-test" nil ".org")))
+ (unwind-protect
+ (progn
+ (with-temp-file file
+ (insert "* Heading\n\nProse before.\n\n"
+ wot-test--wide-input
+ "\nProse after.\n"))
+ (wot-process-file file 40)
+ (let ((content (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))))
+ (should (string-match-p (regexp-quote wot-test--wide-expected) content))
+ (should (string-match-p "Prose before\\." content))
+ (should (string-match-p "Prose after\\." content))))
+ (delete-file file))))