fix: harden post-audit contracts and test gates

This commit is contained in:
Kinneyzhang 2026-08-20 07:34:03 +08:00
parent 183f256020
commit 6a8c7e04f5
26 changed files with 518 additions and 34 deletions

View File

@ -167,8 +167,5 @@ jobs:
shell: pwsh
run: >
emacs -Q --batch -L . -L tests
-l tests/ekp-tests.el
-l tests/ekp-buffer-tests.el
-l tests/ekp-gui-tests.el
-l tests/ekp-c-tests.el
--eval "(load-file \"tests/load-project-source.el\")"
-f ert-run-tests-batch-and-exit

View File

@ -2,6 +2,9 @@
## phase-kp-overhaul-20260726
- 2026-08-20 lock post-audit hardening goals and tasks (`task037``task042`,
`task030` continuation; `issue022``issue028` opened):
`.phrase/phases/phase-kp-overhaul-20260726/change_post_audit_hardening_20260820.md`
- 2026-08-13 close issues awaiting user-visible confirmation (`issue011`
`issue017`, `issue019`, `issue020`; `issue018`/`task030` remain open):
`.phrase/phases/phase-kp-overhaul-20260726/change_close_user_confirmation_20260813.md`

View File

@ -21,3 +21,10 @@
- issue019 [x] [Per-edit frontier replanning destabilizes live projection and cannot restore reversible edits exactly.](../phases/phase-kp-overhaul-20260726/issue_stable_live_edit_transaction_20260729.md)
- issue020 [x] [C-backed resize reflow still takes roughly 6070 ms.](../phases/phase-kp-overhaul-20260726/issue_c_resize_latency_20260729.md)
- issue021 [x] [Final-pass emergency layout can isolate CJK source lines at narrow widths.](../phases/phase-kp-overhaul-20260726/issue_overlong_no_break_atom_20260801.md)
- issue022 [ ] [Invalid buffer widths mutate or create invalid projection state.](../phases/phase-kp-overhaul-20260726/issue_buffer_width_atomicity_20260820.md)
- issue023 [ ] [Backward deletion can republish a stable live projection.](../phases/phase-kp-overhaul-20260726/issue_live_backward_projection_20260820.md)
- issue024 [ ] [Hyphenation locale spelling can select the wrong dictionary.](../phases/phase-kp-overhaul-20260726/issue_hyphen_locale_20260820.md)
- issue025 [x] [C direct API accepts invalid break positions.](../phases/phase-kp-overhaul-20260726/issue_c_position_validation_20260820.md)
- issue026 [ ] [Showcase loses automatic inline policy and test runners can false-green.](../phases/phase-kp-overhaul-20260726/issue_showcase_test_freshness_20260820.md)
- issue027 [x] [Manual integrations remain installed with no projection owner.](../phases/phase-kp-overhaul-20260726/issue_integration_no_projection_20260820.md)
- issue028 [x] [Locked live evaluator can measure stale candidate bytecode.](../phases/phase-kp-overhaul-20260726/issue_live_evaluator_source_freshness_20260820.md)

View File

@ -0,0 +1,53 @@
# Change: Post-Audit Hardening 2026-08-20
## 2026-08-20 — Lock follow-up goals and task boundaries
- **Add** `plan_post_audit_hardening_20260820.md` with the evidence-backed
scope, non-goals, acceptance gates, and stop condition for the post-audit
defects.
- **Add** `task_post_audit_hardening_20260820.md` with `task037``task042`
and the remaining `task030` performance gate.
- **Add** issue records for each user-visible or public-contract defect before
implementation, including `issue028` for the stale-bytecode evaluator
false-green.
- **Behavior/Risk:** documentation-only planning. Closed historical issues
remain closed; `issue018` remains open until its locked gate is met.
## Verification
- Next task ID is `task037`; no task ID is reused.
- The plan explicitly separates buffer ownership, live semantics, dictionary
lookup, C validation, test gates, integration lifecycle, and performance.
- No production or test source is changed in this planning pass.
## 2026-08-20 — Implement correctness and gate hardening
- **Modify** `ekp-buffer.el` and `tests/ekp-buffer-tests.el` for preflight
width validation, backward-delete live stability, and no-projection
integration cleanup (`task037`, `task038`, `task042`; `issue022`,
`issue023`, `issue027`).
- **Modify** `ekp-hyphen.el` and `tests/ekp-tests.el` for exact normalized
locale lookup (`task039`, `issue024`).
- **Modify** `ekp_c/ekp.c` and `tests/ekp-c-tests.el` for sorted, unique,
in-range break-position validation in direct and batch APIs (`task040`,
`issue025`), with the 15-field ABI unchanged.
- **Modify** the showcase, source loader, test runners, Windows CI entry,
and complete ERT selection to remove stale-bytecode false greens and
restore automatic inline-code verification (`task041`, `issue026`).
- **Modify** the live evaluator script/bootstrap to load source from the
selected baseline/candidate root (`task030`, `issue028`). The corrected
narrowed matrix still exceeds the source 16 ms target, so `issue018` and
`task030` remain open.
## Verification
- Source-first normal ERT: 294/294.
- Source-first seeded random ERT: 294/294, including all nine GUI verifier
tests previously omitted by the name filter.
- Per-test isolated ERT: 294/294 process runs logged `0 unexpected`.
- C focused ERT: 15/15; C portable build warning-clean; source fuzz:
300/300 with zero failures; checkdoc, release, dictionary, and shell gates
pass.
- Corrected narrowed source evaluator: parity, zero-work, GC, conflicts, and
width non-regression pass, but source candidate p99 remains above 16 ms;
the performance issue is intentionally not closed.

View File

@ -0,0 +1,23 @@
# Issue: Buffer Width Validation and Failure Atomicity 2026-08-20
## issue022 [ ] Invalid buffer widths mutate or create invalid projection state
- **Status:** Runtime fix verified by `task037`; user-visible confirmation
remains pending.
- **Summary:** `ekp-justify-region` accepts zero, negative, and non-integer
widths without the string API's positive-integer validation. It clears the
existing projection before a later type error, so a rejected request can
destroy valid visible state.
- **Repro:** In a justified buffer, call `ekp-justify-region` with `0`, `-1`,
`1.5`, or `"80"`.
- **Expected vs Actual:** Expected a preflight signal with the old projection
unchanged. Actual zero creates a span with width zero; a type error after a
prior projection leaves no spans.
- **Related:** `ekp-buffer.el`, `ekp.el`, `task037`.
- **Fix:** `ekp-justify-region` now validates the width before font-lock or
projection clearing.
- **Verification:** Red/green focused ERT 1/1 and source-first full ERT
294/294.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,19 @@
# Issue: C Position Vector Validation 2026-08-20
## issue025 [ ] C direct API accepts invalid break positions
- **Status:** Closed by direct contract verification on 2026-08-20.
- **Summary:** The C boundary validates vector shape and signed integer type,
but not the exclusive break-index range, monotonic ordering, or duplicate
policy of hyphen/forbidden positions.
- **Expected vs Actual:** Malformed direct arguments should signal
`ekp-c-invalid-input` per the C API contract. Current `[-1]` and `[99]`
vectors return a layout result instead.
- **Related:** `ekp_c/ekp.c`, `ekp_c/ekp_kp.c`, `task040`.
- **Fix:** Shared preflight now rejects out-of-range, duplicate, and
non-increasing vectors for both direct and batch calls.
- **Verification:** Direct/batch C ERT 15/15, portable warning-clean build,
300-case fuzz, and Elisp parity pass.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer implementation and contract verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,20 @@
# Issue: Hyphenation Locale Resolution 2026-08-20
## issue024 [ ] Hyphenation accepts common locale spelling but selects the wrong dictionary
- **Status:** Runtime fix verified by `task039`; user-visible confirmation
remains pending.
- **Summary:** `de-CH` and `de-DE` normalize to underscore forms but are
looked up in the short-code registry, where they can resolve to the first
arbitrary German dictionary rather than the exact locale.
- **Expected vs Actual:** Equivalent `de_CH`/`de-CH` spellings should select
`hyph_de_CH.dic`; current `de-CH` selects `hyph_de_AT.dic`.
- **Related:** `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`,
`task039`.
- **Fix:** Raw and normalized exact locale keys are checked before the
documented short-code fallback.
- **Verification:** Locale-equivalence ERT 11/11 and full dictionary gates
pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,20 @@
# Issue: No-Projection Integration Lifecycle 2026-08-20
## issue027 [ ] Manual integrations remain installed with no projection owner
- **Status:** Closed by lifecycle verification on 2026-08-20.
- **Summary:** A manual justify request that produces no spans, such as an
empty or foreign-display-only buffer, still installs EKP change hooks and
the substring filter even though auto-mode is disabled.
- **Expected vs Actual:** Integrations should have one active owner: spans or
auto-mode. Actual `spans=nil` still leaves the filter and hooks installed
until a later change happens to trigger cleanup.
- **Related:** `ekp-buffer.el`, `plan_integration_lifecycle_20260728.md`,
`task042`.
- **Fix:** Manual justification installs integrations only when a span or
auto-mode owner exists.
- **Verification:** Empty/foreign-only lifecycle ERT 2/2 and full buffer
suite pass.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer implementation and lifecycle verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,22 @@
# Issue: Backward Live Projection Stability 2026-08-20
## issue023 [ ] Backward deletion can republish a stable projection
- **Status:** Runtime fix verified by `task038`; user-visible confirmation
remains pending.
- **Summary:** Live row crossing treats forward typing and backward deletion
identically. After a forward crossing, deleting a suffix into the previous
native row can republish earlier rows and visibly move already typed text.
- **Expected vs Actual:** The current live-edit contract preserves unaffected
committed projection anchors during local tail shrink. Actual
`ekp-buffer--live-row-crossed-p` triggers a full prefix publication in both
directions.
- **Related:** `postmortem/20260730-incremental-live-append-ownership.md`,
`ekp-buffer.el`, `task038`.
- **Fix:** A live row crossing now commits only when the native row moves
forward; backward tail shrink keeps the transaction local.
- **Verification:** Public edit-path regression and source-first full/random/
isolated ERT all pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,20 @@
# Issue: Live Evaluator Source Freshness 2026-08-20
## issue028 [ ] Locked live evaluator can measure stale candidate bytecode
- **Status:** Closed by source-fresh evaluator verification on 2026-08-20;
performance gate remains open under the now-correct source measurement.
- **Summary:** `run-live-commit-evaluator.sh` loaded the candidate by name and
allowed an ignored local `.elc` to win. A report could therefore claim the
locked source-instrumented 16 ms gate passed while measuring bytecode.
- **Expected vs Actual:** Every baseline and candidate round must explicitly
`load-file` the four production source files for its code root. Before the
bootstrap fix, a narrowed matrix reported `pass:true`; afterward it reports
source candidate p99 above 16 ms and keeps `issue018` open.
- **Related:** `tests/run-live-commit-evaluator.sh`,
`tests/ekp-live-commit-evaluator.el`, `issue018`, `task030`.
- **Verification:** Source-load bootstrap, narrowed matrix, and source-path
live benchmark are complete; full locked matrix remains the task030 gate.
- **Resolved At:** 2026-08-20.
- **Resolved By:** Developer evaluator verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,25 @@
# Issue: Showcase Contract and Test Freshness 2026-08-20
## issue026 [ ] Showcase loses automatic inline policy and runners can false-green
- **Status:** Runtime/test fix verified by `task041`; user-visible
confirmation remains pending.
- **Summary:** The showcase's automatic inline-code face was commented out,
so clean source runs fail the GUI policy contract. Local stale ignored
`tests/*.elc` files can mask the regression, and random/isolated selectors
omit nine GUI verifier tests.
- **Expected vs Actual:** Clean, source-fresh normal/random/isolated runners
should execute the complete ERT inventory and retain the automatic inline
example. Current source has 3 failing showcase tests; stale local bytecode
reports a false green.
- **Related:** `tests/ekp-showcase.el`, `tests/run-tests.sh`,
`tests/run-tests-random-order.el`, `tests/run-tests-isolated.sh`,
`task041`.
- **Fix:** The automatic inline face is restored; one source loader uses
`load-file` for every production/test source and all runners select the
complete ERT registry.
- **Verification:** Source-first showcase 3/3, normal/random/isolated 294-test
inventories, and GUI contract checks pass.
- **Resolved At:** Pending user-visible confirmation.
- **Resolved By:** Developer implementation and verification.
- **Commit:** Pending hardening commit.

View File

@ -0,0 +1,82 @@
# Plan: Post-Audit Hardening 2026-08-20
## Context
The repository audit after `183f256` found runtime and gate defects that are
independent of the already accepted text-property layout architecture. This
plan stays inside `phase-kp-overhaul-20260726`; it does not reopen closed
issues or weaken the source-clean display contract.
## Goals
1. Make public buffer width errors fail before projection mutation and keep
the previous projection intact on every rejected request.
2. Define and test the backward-delete live projection invariant before
changing the row-crossing owner.
3. Resolve locale spellings to the exact bundled dictionary before short-code
fallback, and reject malformed C position vectors at the module boundary.
4. Restore the showcase's automatic inline-code contract and make every test
runner use current source and the complete ERT set.
5. Remove manual buffer integrations when no projection or auto-mode owner
remains.
6. Re-profile `issue018`/`task030` after the correctness fixes with a
source-fresh evaluator and close it only if the locked source-instrumented
16 ms contract is actually met.
## Scope and milestones
- M1 (`task037`, `task042`): buffer width validation, failure atomicity, and
integration lifecycle.
- M2 (`task038`): backward-delete projection semantics and a regression that
drives the public edit path.
- M3 (`task039`, `task040`): exact hyphenation locale lookup and complete C
position-vector validation, with direct boundary tests.
- M4 (`task041`): showcase automatic inline face, source-first test loading,
and complete random/isolated selection.
- M5 (`task030`, `issue028`): make the locked width/engine matrix source-fresh,
then optimize only the remaining structural commit owner.
## Non-goals
- Do not change valid KP output, the 15-field C ABI, or the source-clean
buffer representation.
- Do not hide latency with debounce, stale layout reuse, skipped publication,
global GC changes, or test-only exceptions.
- Do not close `issue018` from byte-compiled production numbers alone.
- Do not rewrite historical postmortems; append current decisions and
verification evidence.
## Acceptance gates
- Invalid or non-positive buffer widths signal before any owned property,
span, filter, hook, point, mark, or modified-state change.
- Backward deletion either preserves the committed projection as specified or
has an explicit revised contract documented and tested through the public
command path.
- `de-CH`/`de_CH` and equivalent exact locale spellings select the same exact
dictionary; unknown locales use the documented short-code fallback.
- Out-of-range, unsorted, or duplicate C hyphen/forbidden positions signal
`ekp-c-invalid-input`; valid arrays retain exact Elisp/C parity.
- Clean source, stale bytecode, normal, random, and isolated test runners
all exercise the same complete current ERT inventory and the showcase's
automatic inline contract.
- The live evaluator explicitly loads source from both baseline and candidate
roots; a bytecode-backed `pass:true` is not valid evidence.
- Full ERT, fuzz, C build/tests, byte compilation, checkdoc, package-lint,
release, dictionary, and focused performance gates pass before closure.
## Risks and dependencies
- Backward-delete behavior is a product invariant, not a local condition;
implementation must follow the existing spec and real public-path evidence.
- C position validation must match the DP's exclusive break-index domain and
preserve sorted-vector assumptions used by binary search.
- The performance gate depends on trustworthy source loading; stale ignored
`.elc` files must not influence any result.
## Stop condition
Stop only when all tasks in this plan have current issue/task evidence,
closed issues have user-visible or contract-level verification, `issue018`
is either genuinely closed by its locked gate or explicitly remains open with
fresh measurements, and the complete diff is committed and reviewed.

View File

@ -0,0 +1,56 @@
# Tasks: Post-Audit Hardening 2026-08-20
- task037 [x] Validate public buffer widths before mutation and make rejected
`ekp-justify-region` requests failure-atomic. Add red tests for zero,
negative, non-integer, and projection-preservation cases.
- Source: post-audit buffer probe and `plan_post_audit_hardening_20260820.md`.
- Validation: focused buffer ERT 4/4 plus source-preferred full ERT 294/294.
- task038 [x] Resolve backward-delete live projection semantics. Add a public
edit-path regression at the narrowest reproducible width and update the
live-layout spec/postmortem with the accepted invariant.
- Source: `postmortem/20260730-incremental-live-append-ownership.md` and
`plan_post_audit_hardening_20260820.md`.
- Validation: focused live-edit ERT 4/4, randomized ERT 294/294, and
source-first isolated ERT 294/294 process runs.
- task039 [x] Make hyphenation locale resolution try the exact normalized
locale registry before short-code fallback. Add equivalent BCP-47/underscore
tests and preserve fail-closed unsupported-pattern behavior.
- Source: `ekp-hyphen.el`, `plan_dictionary_governance_20260728.md`, and
the post-audit locale probe.
- Validation: focused dictionary ERT 11/11, manifest check, and full ERT
294/294.
- task040 [x] Validate C hyphen and forbidden position vectors for the exact
exclusive break-index domain, monotonic ordering, and duplicate policy
before DP access. Add direct single/batch ABI regressions without changing
the 15-field schema.
- Source: `plan_c_api_contract_20260728.md` and the post-audit C probe.
- Validation: C warning-clean build, direct/batch C ERT 15/15, fuzz
300/300, and Elisp parity.
- task041 [x] Restore the showcase automatic inline-code face and make normal,
random, and isolated runners source-fresh and complete over the ERT
inventory. Add a regression proving stale ignored bytecode cannot mask the
showcase contract.
- Source: `888a401`, `plan_test_isolation_20260728.md`, and the post-audit
clean-source failure.
- Validation: source loader symbol checks, clean-source showcase 3/3,
normal/random/isolated 294-test inventories, and GUI contract checks.
- task042 [x] Remove manual buffer integrations when no spans remain and
auto-mode does not own the lifecycle. Preserve composition with foreign
filters and add empty/foreign-only teardown regressions.
- Source: `plan_integration_lifecycle_20260728.md` and the post-audit
no-projection probe.
- Validation: focused integration ERT 4/4 and full source-first buffer
suite.
- task030 [ ] Re-profile and, if still necessary, optimize the remaining
source-instrumented narrow live-append structural commit path after
task037task042. Keep exact parity, source-clean projection, and zero-work
point motion as hard constraints; close only on the locked 16 ms gate.
- `issue028` is part of this task: each baseline/candidate evaluator round
must explicitly load source files from its own code root, never rely on
local `.elc` precedence.

View File

@ -905,6 +905,7 @@ EFFECTIVE-END includes the source boundary owned by the line."
(and current-prefix-arg
(prefix-numeric-value current-prefix-arg))))))
(setq pixel (or pixel (ekp-buffer--effective-width)))
(ekp--validate-width pixel)
(let ((start (min beg end))
(finish (max beg end)))
(setq beg start
@ -925,7 +926,9 @@ EFFECTIVE-END includes the source boundary owned by the line."
(setq ekp-buffer--conflicts nil)
(dolist (range (ekp-buffer--paragraph-ranges beg end))
(ekp-buffer--layout-paragraph (car range) (cdr range) pixel))
(if (or ekp-auto-justify-mode ekp-buffer--spans)
(ekp-buffer--install-integrations)
(ekp-buffer--remove-integrations))
(goto-char point-before)
(when mark-before
(set-marker (mark-marker) (marker-position mark-before)))
@ -1939,7 +1942,7 @@ When START and FINISH are non-nil, copy only that source interval."
(native (and end (ekp-buffer--native-row-start end))))
(and start end
(if native
(/= native start)
(> native start)
(and (< start end)
(> (ekp--measured-width
(ekp-buffer--logical-substring start end))

View File

@ -73,22 +73,29 @@ LEFT/RIGHT: minimum chars before first / after last break."
;;; Dictionary Loading
(defun ekp-hyphen--normalize-lang (lang)
"Return the normalized registry key for LANG."
(downcase (replace-regexp-in-string "-" "_" lang)))
(defun ekp-hyphen-load-languages (dir)
"Scan DIR for .dic files, populate language registry."
(dolist (file (directory-files dir t "\\.dic\\'"))
(let* ((name (file-name-nondirectory file))
(lang (replace-regexp-in-string "\\(^hyph_\\|\\.dic$\\)" "" name))
(normalized (ekp-hyphen--normalize-lang lang))
(short (car (split-string lang "_"))))
(puthash lang file ekp-hyphen--langs)
(puthash normalized file ekp-hyphen--langs)
(unless (gethash short ekp-hyphen--langs-short)
(puthash short file ekp-hyphen--langs-short)))))
(defun ekp-hyphen--resolve-lang (lang)
"Resolve LANG to dictionary path, trying exact then short forms."
(or (gethash lang ekp-hyphen--langs)
(let* ((norm (downcase (replace-regexp-in-string "-" "_" lang)))
(let* ((norm (ekp-hyphen--normalize-lang lang))
(parts (split-string norm "_"))
found)
(setq found (gethash norm ekp-hyphen--langs))
(while (and parts (not found))
(setq found (gethash (string-join parts "_")
ekp-hyphen--langs-short)

View File

@ -94,6 +94,22 @@ static ptrdiff_t paragraph_field_length(int field, ptrdiff_t prefix_len)
}
}
static bool strictly_increasing_positions_p(emacs_env *env, emacs_value vector,
int32_t minimum, int32_t maximum)
{
ptrdiff_t length = env->vec_size(env, vector);
int32_t previous = minimum - 1;
for (ptrdiff_t i = 0; i < length; i++) {
int32_t position = clamp32(env->extract_integer(
env, env->vec_get(env, vector, i)));
if (position < minimum || position > maximum || position <= previous)
return false;
previous = position;
}
return true;
}
static const char *validate_paragraph_shapes(emacs_env *env, emacs_value *args)
{
static const int vectors[] = {0, 1, 2, 3, 4, 5, 6, 9, 10, 11, 12};
@ -134,6 +150,11 @@ static const char *validate_paragraph_values(emacs_env *env, emacs_value *args)
if (env->extract_integer(env, args[7]) < 0 ||
env->extract_integer(env, args[13]) < 0)
return "EKP C hyphen width and protrusion must be nonnegative";
int32_t box_count = clamp32(env->vec_size(env, args[0]) - 1);
if (!strictly_increasing_positions_p(env, args[6], 0, box_count - 1))
return "EKP C hyphen positions must be unique sorted box indices";
if (!strictly_increasing_positions_p(env, args[11], 1, box_count))
return "EKP C forbidden positions must be unique sorted gap indices";
return NULL;
}

View File

@ -827,7 +827,7 @@
(1- (point)) 'ekp-buffer--display))))))
(ert-deftest ekp-buffer-test-live-backward-wrap-crossing-commits-once ()
"Deleting into the previous native row publishes one atomic plan."
"Deleting into the previous native row keeps the live transaction local."
(let ((text
"alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu")
(calls 0)
@ -845,8 +845,8 @@
(setq calls (1+ calls))
(apply original arguments))))
(call-interactively #'delete-backward-char)
(should (= calls 1))
(should-not ekp-buffer--live-edit)
(should (= calls 0))
(should ekp-buffer--live-edit)
(should (equal (substring-no-properties (buffer-string))
(concat text " xy"))))))))
@ -2421,6 +2421,42 @@ keeps the buffer justified, and leaves it unmodified."
(should-error (call-interactively #'ekp-justify-region)
:type 'buffer-read-only)))
(ert-deftest ekp-buffer-test-justify-invalid-width-preserves-projection ()
"Invalid manual widths signal before clearing an existing projection."
(ekp-buffer-test--with-text "invalid width must preserve this projection"
(ekp-justify-region (point-min) (point-max) 20)
(let ((before (buffer-substring (point-min) (point-max)))
(spans ekp-buffer--spans))
(dolist (width '(0 -1 1.5 "80"))
(should-error (ekp-justify-region (point-min) (point-max) width)
:type 'user-error)
(should (equal-including-properties
(buffer-substring (point-min) (point-max)) before))
(should (eq ekp-buffer--spans spans))))))
(ert-deftest ekp-buffer-test-justify-empty-removes-unused-integrations ()
"Manual justification does not retain hooks when no span was installed."
(ekp-buffer-test--with-text ""
(ekp-justify-region (point-min) (point-max) 20)
(should-not ekp-buffer--spans)
(should-not ekp-buffer--filter-installed)
(should-not (memq #'ekp-buffer--before-change
before-change-functions))
(should-not (memq #'ekp-buffer--after-layout-change
after-change-functions))))
(ert-deftest ekp-buffer-test-justify-foreign-only-removes-unused-integrations ()
"A foreign-only paragraph does not retain manual EKP integrations."
(ekp-buffer-test--with-text "foreign ownership keeps this paragraph natural"
(put-text-property (point-min) (point-max) 'display "foreign")
(ekp-justify-region (point-min) (point-max) 20)
(should-not ekp-buffer--spans)
(should-not ekp-buffer--filter-installed)
(should-not (memq #'ekp-buffer--before-change
before-change-functions))
(should-not (memq #'ekp-buffer--after-layout-change
after-change-functions))))
;;;; Commands and mode integration
(ert-deftest ekp-buffer-test-no-break-public-commands ()

View File

@ -30,6 +30,12 @@
[0] [0] [0] [] 0 10
[0 0] [0 0] [] [0 0] 0 10))
(defun ekp-c-tests--position-args ()
"Return valid direct C arguments with several position slots."
(list [0 10 20 30] [0 10 20 30] [0 10 20 30]
[0 0 0] [0 0 0] [0 0 0] [0 2] 0 15
[0 0 0 0] [0 0 0 0] [1 3] [0 0 0 0] 0 15))
(ert-deftest ekp-c-test-rejects-non-vector-schema-field ()
"Schema errors signal the module's explicit input condition."
(skip-unless (ekp-c-tests--available))
@ -62,6 +68,33 @@
(should-error (apply #'ekp-c-break-with-arrays args)
:type 'ekp-c-invalid-input)))
(ert-deftest ekp-c-test-rejects-invalid-break-position-vectors ()
"Position vectors must be in range, strictly sorted, and unique."
(skip-unless (ekp-c-tests--available))
(dolist (case '((6 [-1]) (6 [3]) (6 [2 1]) (6 [1 1])
(11 [0]) (11 [4]) (11 [2 1]) (11 [2 2])))
(let ((args (ekp-c-tests--position-args)))
(setf (nth (car case) args) (cadr case))
(should-error (apply #'ekp-c-break-with-arrays args)
:type 'ekp-c-invalid-input))))
(ert-deftest ekp-c-test-accepts-position-boundaries ()
"Valid edge positions preserve the direct API result contract."
(skip-unless (ekp-c-tests--available))
(let* ((args (ekp-c-tests--position-args))
(result (apply #'ekp-c-break-with-arrays args)))
(should (consp result))
(should (listp (car result)))
(should (numberp (cdr result)))))
(ert-deftest ekp-c-test-batch-rejects-invalid-position-vectors ()
"Batch preflight applies the same position contract to every paragraph."
(skip-unless (ekp-c-tests--available))
(let ((args (ekp-c-tests--position-args)))
(setf (nth 11 args) [0])
(should-error (ekp-c-break-batch (vector (vconcat args)))
:type 'ekp-c-invalid-input)))
(ert-deftest ekp-c-test-batch-rejects-short-paragraph-vector ()
"Batch preflight validates each paragraph before indexing 15 fields."
(skip-unless (ekp-c-tests--available))

View File

@ -30,6 +30,9 @@
(defconst ekp-live-commit-evaluator--maximum-regression 15.0)
(defconst ekp-live-commit-evaluator--default-gc-threshold
gc-cons-threshold)
(defconst ekp-live-commit-evaluator--source-files
'("ekp-utils.el" "ekp-hyphen.el" "ekp.el" "ekp-buffer.el")
"Production source files loaded explicitly for every measured root.")
(defconst ekp-live-commit-evaluator--corpus
(concat
" extraordinary editing continues smoothly 中文拉丁混排"
@ -53,6 +56,14 @@
(defvar ekp-live-commit-evaluator--signature-ms 0.0)
(defvar ekp-live-commit-evaluator--cache-ms 0.0)
(defun ekp-live-commit-evaluator--load-source-root ()
"Load every production file from `EKP_LIVE_COMMIT_CODE_ROOT'."
(let ((root (getenv "EKP_LIVE_COMMIT_CODE_ROOT")))
(unless root
(error "EKP_LIVE_COMMIT_CODE_ROOT is required for measurement"))
(dolist (file ekp-live-commit-evaluator--source-files)
(load-file (expand-file-name file root)))))
(defun ekp-live-commit-evaluator--env-number (name fallback)
"Return numeric environment variable NAME, or FALLBACK."
(if-let ((value (getenv name)))
@ -692,6 +703,7 @@
(if (equal (getenv "EKP_LIVE_COMMIT_MODE") "compare")
(ekp-live-commit-evaluator--compare)
(ekp-live-commit-evaluator--load-source-root)
(ekp-live-commit-evaluator--write-record
(ekp-live-commit-evaluator--measure-round)))

View File

@ -47,10 +47,9 @@
(mixed (concat "中英混排是 Emacs 里最常见的场景: The quick brown fox "
"jumps over the lazy dog, 而 internationalization "
"这样的长词在窄栏会按 Liang 模式断词。自动行内代码 "
(propertize
"(ekp-pixel-justify paragraph-text target-width 'justify nil)"
;; (propertize
;; "(ekp-pixel-justify paragraph-text target-width 'justify nil)"
;; 'face 'ekp-showcase-inline-code)
'face 'ekp-showcase-inline-code)
" 可以在合法空白边界附近换行,但不会插入"
" discretionary hyphen。"))
(atoms (concat "行内原子演示:手动硬原子 "

View File

@ -279,7 +279,16 @@ Used to verify no content is lost by justification."
(ert-deftest ekp-test-hyphen-lang-fallback ()
"Short language codes resolve to a dictionary."
(should (ekp-hyphen-create "en"))
(should-error (ekp-hyphen-create "zz_XX")))
(dolist (locale '("zz_XX" "zz-XX"))
(should-error (ekp-hyphen-create locale)
:type 'ekp-hyphen-dictionary-not-found)))
(ert-deftest ekp-test-hyphen-normalized-locale-prefers-exact-dictionary ()
"Equivalent locale spellings resolve before the short-code fallback."
(dolist (locale '("de_CH" "de-CH" "de_ch" "DE-CH"))
(should (equal (file-name-nondirectory
(ekp-hyphen--resolve-lang locale))
"hyph_de_CH.dic"))))
(ert-deftest ekp-test-hyphen-alternative-languages-fail-closed ()
"Replacement-pattern dictionaries must not degrade to plain Liang breaks.
@ -302,6 +311,8 @@ DP represents break-specific replacement widths."
;; Esperanto also contains slash-prefixed patterns whose libhyphen
;; meaning is not representable as an ordinary Liang pattern.
(should-error (ekp-hyphen-create "eo")
:type 'ekp-hyphen-unsupported-pattern)
(should-error (ekp-hyphen-create "hu-HU")
:type 'ekp-hyphen-unsupported-pattern))
(ert-deftest ekp-test-hyphen-alternative-error-reaches-public-dispatch ()

View File

@ -0,0 +1,27 @@
;;; load-project-source.el --- Load EKP test subjects from source -*- lexical-binding: t; -*-
;;; Commentary:
;; Test bootstrap that prevents ignored or stale byte-code files from deciding
;; which implementation the suite exercises.
;;; Code:
(let ((root (file-name-directory
(directory-file-name
(file-name-directory (or load-file-name buffer-file-name))))))
(dolist (file '("ekp-utils.el"
"ekp-hyphen.el"
"ekp.el"
"ekp-buffer.el"
"tests/ekp-showcase.el"
"tests/ekp-gui-verify.el"
"tests/ekp-tests.el"
"tests/ekp-buffer-tests.el"
"tests/ekp-gui-tests.el"
"tests/ekp-c-tests.el"
"tests/run-tests-random-order.el"))
(load-file (expand-file-name file root))))
(provide 'load-project-source)
;;; load-project-source.el ends here

View File

@ -42,6 +42,7 @@ run_round()
EKP_LIVE_COMMIT_LABEL=$label \
EKP_LIVE_COMMIT_ROUND=$round \
EKP_LIVE_COMMIT_OUTPUT=$output \
EKP_LIVE_COMMIT_CODE_ROOT=$code_root \
"$EMACS_BIN" -Q --batch -L "$code_root" -L "$ROOT/tests" \
-l "$ROOT/tests/ekp-live-commit-evaluator.el"
}

View File

@ -9,20 +9,14 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TESTS=$(
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
-l "$ROOT/tests/ekp-tests.el" \
-l "$ROOT/tests/ekp-buffer-tests.el" \
-l "$ROOT/tests/ekp-gui-tests.el" \
-l "$ROOT/tests/ekp-c-tests.el" \
--eval '(dolist (test (ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t))
-l "$ROOT/tests/load-project-source.el" \
--eval '(dolist (test (ert-select-tests t t))
(princ (format "%s\n" (ert-test-name test))))'
)
for test_name in $TESTS; do
echo "isolated ERT: $test_name"
"$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
-l "$ROOT/tests/ekp-tests.el" \
-l "$ROOT/tests/ekp-buffer-tests.el" \
-l "$ROOT/tests/ekp-gui-tests.el" \
-l "$ROOT/tests/ekp-c-tests.el" \
-l "$ROOT/tests/load-project-source.el" \
--eval "(ert-run-tests-batch-and-exit '$test_name)"
done

View File

@ -26,7 +26,7 @@
(interactive)
(let* ((seed (string-to-number (or (getenv "EKP_TEST_SEED") "20260728")))
(tests
(ert-select-tests "^ekp-\\(?:test\\|[[:alnum:]-]+-test\\)-" t))
(ert-select-tests t t))
(names (mapcar #'ert-test-name tests))
(selector
(cons 'member (ekp-tests--permuted-names names seed))))

View File

@ -9,19 +9,12 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
case "$ORDER" in
"")
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
-l "$ROOT/tests/ekp-tests.el" \
-l "$ROOT/tests/ekp-buffer-tests.el" \
-l "$ROOT/tests/ekp-gui-tests.el" \
-l "$ROOT/tests/ekp-c-tests.el" \
-l "$ROOT/tests/load-project-source.el" \
-f ert-run-tests-batch-and-exit
;;
--random-order)
exec "$EMACS" -Q --batch -L "$ROOT" -L "$ROOT/tests" \
-l "$ROOT/tests/ekp-tests.el" \
-l "$ROOT/tests/ekp-buffer-tests.el" \
-l "$ROOT/tests/ekp-gui-tests.el" \
-l "$ROOT/tests/ekp-c-tests.el" \
-l "$ROOT/tests/run-tests-random-order.el" \
-l "$ROOT/tests/load-project-source.el" \
-f ekp-tests-run-random-order
;;
*)