diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-11 14:25:55 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-11 14:25:55 -0500 |
| commit | 8d790f371e54a8cc3e79a5ce72cd4dd5b3fa4513 (patch) | |
| tree | ff6c1a496c4e7727bd823979a582dc21ef25b811 /.ai/scripts/tests | |
| parent | bdc9a5d6e1320032770f54c747c210e4f465c399 (diff) | |
| download | rulesets-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.el | 42 | ||||
| -rw-r--r-- | .ai/scripts/tests/test-wrap-org-table.el | 188 |
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)))) |
