diff --git a/.github/workflows/n1-desktop-smoke.yml b/.github/workflows/n1-desktop-smoke.yml index dea48a2a..5318a8ce 100644 --- a/.github/workflows/n1-desktop-smoke.yml +++ b/.github/workflows/n1-desktop-smoke.yml @@ -1,10 +1,9 @@ -name: I23 real B26 native TikZ and responsive whole-page PDF acceptance +name: I33 real B30 source-history Undo-while-held native TikZ PDF acceptance on: push: branches: - - qa/i23-b25-live-fullpage-x11 - - feat/b25-egui-pointer-focus-fit-page-20261101 - - feat/b16-egui-live-palette-six-gesture-20261031 + - qa/i26-b27-live-undo-x11 + - feat/b27-egui-undo-held-pointer-20261101 - main paths: - '.github/workflows/n1-desktop-smoke.yml' @@ -17,8 +16,7 @@ on: - 'Cargo.lock' pull_request: branches: - - feat/b25-egui-pointer-focus-fit-page-20261101 - - feat/b16-egui-live-palette-six-gesture-20261031 + - feat/b27-egui-undo-held-pointer-20261101 - main paths: - '.github/workflows/n1-desktop-smoke.yml' @@ -33,7 +31,7 @@ on: permissions: contents: read concurrency: - group: i23-desktop-${{ github.event.pull_request.head.sha || github.sha }} + group: i33-desktop-${{ github.event.pull_request.head.sha || github.sha }} cancel-in-progress: true jobs: desktop: @@ -54,7 +52,7 @@ jobs: run: | test "$(git rev-parse HEAD)" = "$TIKZARIUM_SOURCE_SHA" echo "TIKZARIUM_APP_TREE_SHA=$(git rev-parse HEAD:crates/tikzarium-app/src)" >> "$GITHUB_ENV" - test "$(git rev-parse HEAD:crates/tikzarium-app/src)" = '47c4c86be10471a5f56f7019ad63a77e7f4480f5' + test "$(git rev-parse HEAD:crates/tikzarium-app/src)" = '965933b3f70504f2f4082bc22795bab67fad5d44' - name: Install isolated hosted GUI and external XeLaTeX prerequisites run: | sudo apt-get update -q @@ -78,7 +76,7 @@ jobs: cp Cargo.lock "$RUNNER_TEMP/i19-build-evidence/Cargo.lock" echo "::warning::Source repository has NO Cargo.lock; generated resolution archived, release reproducibility NOT VERIFIED" fi - - name: Real B26 X11 gestures, GUI Save, XeLaTeX PDF, Fit page and resize + - name: Real B30 X11 source-history held-pointer Undo, six gestures, GUI Save/XeLaTeX, fit/resize run: | mkdir -p "$RUNNER_TEMP/i19-runtime" chmod 700 "$RUNNER_TEMP/i19-runtime" @@ -89,7 +87,7 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - name: i23-b26-desktop-${{ env.TIKZARIUM_SOURCE_SHA }} + name: i33-b30-desktop-${{ env.TIKZARIUM_SOURCE_SHA }} path: | ${{ runner.temp }}/i19-evidence/*.json ${{ runner.temp }}/i19-evidence/*.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8500b8..b6eeba0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## Unreleased — I36 B30 X11 held-button AND keyboard receipt desktop acceptance (NOT_RUN pending artifacts) + +- I35 [actual Linux/X11 41279880858](https://github.com/i-shutov/tikzarium/actions/runs/41279880858) held **Button1Mask=256** across key-only XTest Ctrl+Z but source Undo still failed and later release inserted Line #2. I36 adds 80ms XTest key dwell/XSync, native XQueryKeymap proof of Ctrl/z down/up, XGetInputFocus snapshot, server Button1Mask check each key step, preserving strict source rollback. This disambiguates too-fast synthetic key events from B30 production routing. QA-only; actual I36 GUI acceptance NOT_RUN until complete, physical OS NOT_RUN, no release. + +## Unreleased — I35 B30 true XTest held Ctrl+Z and GUI acceptance (HISTORICAL FAIL) + +- [I34 real X11 41279540150](https://github.com/i-shutov/tikzarium/actions/runs/41279540150) correctly failed closed: X11 Button1Mask **256→0** across `xdotool key --clearmodifiers` before QA mouseup, so old “held” ghost tests used invalid input. I35 replaces that key with genuine XTestFakeKeyEvent key-only Ctrl+Z, demonstrated under isolated Xvfb to preserve Button1Mask=256, and keeps server mask/byte-exact/no-ghost tests. No B production edits; actual I35 acceptance and physical Win10/M1/M4 NOT_RUN, no merge/release. + +## Unreleased — I34 B30 real held-pointer XQueryPointer forensic acceptance (QA_HARNESS FAIL) + +- I33 actual [X11 41279059557](https://github.com/i-shutov/tikzarium/actions/runs/41279059557) **FAIL**: before explicit driver mouseup, Ctrl+Z screenshot reveals ghost Line #2; saved .tex contains Line IDs 1+2, matching previous B29 source SHA exactly. QA now queries actual X11 Button1Mask before/after key to distinguish XTest-button release from application early insertion. Fail closed on absent held button; source-rollback assertions unchanged. B30 production and C catalogue untouched, physical Win10/M1/M4 NOT_RUN; no release. + +## Unreleased — I33 B30 real X11 pointer Undo QA (historical FAIL) + +- First exact B30 QA [41278805656](https://github.com/i-shutov/tikzarium/actions/runs/41278805656) failed **offline fixture-key consistency** before launching the executable; corrected the test reference to the unchanged B30 fixture marker. This is a QA preflight correction, **not** a real GUI FAIL/PASS; strict live test unchanged. + +- QA-only integration at B30 #69 exact `750fb6b743a516f44a362f2f568ce1b92c69e6f4` and source tree `965933b3f70504f2f4082bc22795bab67fad5d44`; real X11 six-tool/held-click and held-drag Undo, exact source restoration without ghosts, Russian F1, GUI XeLaTeX PDF, fit/resize screenshot evidence. Historical B29 GUI [41274957152](https://github.com/i-shutov/tikzarium/actions/runs/41274957152) FAIL. B30 hosted CI 5/5 workflow families PASS does not prove actual GUI; B30 X11 NOT_RUN at commit. Physical Win10/MiKTeX and M1/M4 XeLaTeX NOT_RUN; no release. + +## Unreleased — I31 B29 source-history actual GUI re-acceptance (NOT_RUN) + +- Independently pinned QA source to B29 draft PR #69 HEAD `133cab9eff2da0e2ab2ae9574e5328f2dad3ee62`, app tree `d7a487e91614c7b607ce92f53275e10e2a5ee911`. Previous B28 real X11 [41271432298](https://github.com/i-shutov/tikzarium/actions/runs/41271432298) failed despite apparent Undo success. Re-run real held-click and held-drag Undo plus 6 native tools, GUI Save/Build/Export and F1/PDF screenshot audit without weakening test predicates; no B production changes, old #48 untouched. Hosted B29 CI does not replace actual X11; physical Win10/Mac NOT_RUN. No release. + +## Unreleased — I29 B28 X11 initial source-save gate: explicit GUI Save (pending rerun) + +- [I28 run 41271124460](https://github.com/i-shutov/tikzarium/actions/runs/41271124460) failed before held-pointer tests: actual source editor displayed original Unicode and path but Ctrl+S did not persist the file. Real pointer/PDF NOT_RUN. QA now records shortcut failure separately and uses the visible GUI Save button to test the required source-persistence vertical; no TeX is fabricated and shortcut is not called PASS. Corrected exact-head CI/visual evidence pending. + +## Unreleased — I28 B28 focus-safe pointer Undo real X11 gate (NOT_RUN) + +- QA-only integration onto exact B28 `f2dfbd0bb073e6152146a151eca138af8fc030e1`, source tree `4be7a5aebbdd77a820abc08ea37d349dcab6bcec`. Existing B27 real-X11 FAIL retained. Requires actual Ctrl+Z during a held guide pointer despite TextEdit focus, byte-exact Save rollback, no ghost, 6 native tools, Russian F1, GUI XeLaTeX PDF and independent visual review. Real B28 X11 / physical OS NOT_RUN pending new exact-head evidence. + +## Unreleased — 2026-11-01: I26 B27 physical-input Linux/X11 Undo + six native tools (QA PENDING) + +- QA-only branch based on [#69 B27](https://github.com/i-shutov/tikzarium/pull/69) exact source HEAD `1805a46cbca2985c4b4798ccf4b0e22171107297`, app source tree `5274ea9c12a3a9f25cea3b47c0fa504c1b5fff1b`. No edits to B-owned Rust or the old independent #48 branch. Updated X11 driver sends **real Ctrl+Z while holding a mouse press/drag on the provisional guide**, releases, GUI-saves and checks exact restored source bytes and zero ghost commands; then creates a fresh line, verifies Save and proceeds with all six native TikZ gestures, contextual Russian F1 and actual GUI XeLaTeX PDF/fitting. +- This is **NOT_RUN until exact-head hosted workflow plus independent saved-source/PDF/screenshot artifact review**. Unit-event B27 proof cannot substitute. Hosted X11 is not physical Windows10/MiKTeX or Mac M1/M4/XeLaTeX acceptance; no release. Cargo.lock remains untracked. + + ## Unreleased — 2026-11-01: I24 B26 actual X11 sticky whole-page resize QA (NOT_RUN) - Non-production I23 QA bridge merged with **B26 #67** exact source `73878df418dd6343bbca9dd94a2e5daf979ed7fc`, src tree `47c4c86be10471a5f56f7019ad63a77e7f4480f5`; actual egui six-tool + GUI-save/build/export + Russian F1 and full-PDF visual evidence must be regenerated on B26. Driver now captures a genuine selected Fit Page resize/restore (1180×680 ↔ 1366×740); screenshot difference is **not** a visual PASS, and desktop new-page raster still NOT_RUN. Track exact Cargo.lock resolution separately, do not claim physical Win10/MiKTeX/M1/M4 QA. No release. diff --git a/fixtures/qa/n1-desktop-x11-1366.json b/fixtures/qa/n1-desktop-x11-1366.json index fa9da5e2..1edb3561 100644 --- a/fixtures/qa/n1-desktop-x11-1366.json +++ b/fixtures/qa/n1-desktop-x11-1366.json @@ -1,6 +1,6 @@ { "schema": 1, - "profile": "B26 exact app-tree candidate; B25 screenshot acceptance historical; B26 re-review required", + "profile": "B30 exact app-tree: GUI-native source editor history isolated from visual inverse patch; actual held-click/held-drag Undo, GUI Save and native PDF", "calibration_runs": [ 41192581511, 41193030547, @@ -11,7 +11,7 @@ ], "initial_png_sha256": "b65754a44205040655d6434b8a592b665ec4fa02deac4ff8c4dfc50ddc7f612a", "selected_png_sha256": "61f3d83313300494bc43fa3696c148b2c54011b27682210610d81db1f68f966a", - "layout_note": "B26 reuses B25 button layout and six-gesture coordinates, now with persistent Fit Page on resize/new raster; these coordinates must be accepted by exact-B26 real X11 screenshot evidence, not inherited from B25.", + "layout_note": "B30 only changes Undo focus/native editor history, not known B26/B28 coordinates. Coordinates are hypotheses until this exact B30 real X11 source/PDF/screenshots are independently reviewed; no inherited visual PASS.", "path_click": [ 310, 18 @@ -128,7 +128,7 @@ 698, 13 ], - "reviewed_app_tree_sha": "47c4c86be10471a5f56f7019ad63a77e7f4480f5", + "reviewed_app_tree_sha": "965933b3f70504f2f4082bc22795bab67fad5d44", "zoom_out_click": [ 882, 72 @@ -144,5 +144,7 @@ "restore_size": [ 1366, 740 - ] + ], + "undo_held_pointer": true, + "b30_release_source_history_regression": true } diff --git a/insights/2026-11-01_I26_B27_HELD_UNDO_X11_QA.md b/insights/2026-11-01_I26_B27_HELD_UNDO_X11_QA.md new file mode 100644 index 00000000..16d965ed --- /dev/null +++ b/insights/2026-11-01_I26_B27_HELD_UNDO_X11_QA.md @@ -0,0 +1,42 @@ +# I26 B27 QA initial checkpoint — pending actual X11 acceptance +2026-11-01 · Parent B27 #69 `1805a46cbca2985c4b4798ccf4b0e22171107297`; app source tree `5274ea9c12a3a9f25cea3b47c0fa504c1b5fff1b`. + +Pre-existing B27 exact-head CI: Rust, D QA, N1 app generated-source PDF, genuine hosted MiKTeX and font gate SUCCESS. Its real X11 workflow [41265211038](https://github.com/i-shutov/tikzarium/actions/runs/41265211038) FAILED **in source-tree guard before installing prerequisites or starting GUI** because it was still pinned to B26 `47c4c86b...`. Therefore no B27 real GUI PASS can be inherited. + +I26 changes *only QA, workflow, fixture and plans/insights/CHANGELOG*. Exact-pin the B27 app tree, issue real click-held and drag-held Ctrl+Z before pointer mouse-up, save source through GUI and demand byte-identical original. Then assert fresh native gesture, six-tool Save, Russian F1 and XeLaTeX PDF. Two pure predicate tests guard against accepting a ghost command, unmodified source or unrelated Cyrillic mutation, but do not claim GUI. The new hosted run/artifact, independent screenshot review, successful PR merge and post-merge CI all **NOT_RUN at this writing**; no physical OS tests or release. + +## I28 exact-head checkpoint — new real X11 verification pending + +2026-11-01 · I28 QA integration onto exact B28 `f2dfbd0bb073e6152146a151eca138af8fc030e1` (independently verified app src tree `4be7a5aebbdd77a820abc08ea37d349dcab6bcec`). Real B27 X11 [41267001529](https://github.com/i-shutov/tikzarium/actions/runs/41267001529) was FAIL and remains historical evidence. B28 routes Undo during an armed guide pointer despite stale focused TextEdit, consuming the key; unweakened genuine X11 held-click/held-drag, exact saved-source rollback, fresh gesture, 6 native tools, F1 and GUI Save/Build/Export PDF must pass with independent source/PDF/screenshot review. Unit-event B28 success alone is insufficient. Real B28 X11 NOT_RUN, physical Win10/MiKTeX and Mac M1/M4 NOT_RUN, Cargo.lock untracked; no release. + +## I29 real B28 bootstrap QA correction after failed run 41271124460 + +Independent first B28 exact-head X11 workflow [41271124460](https://github.com/i-shutov/tikzarium/actions/runs/41271124460) **FAIL** before first pointer: actual egui window and real Cyrillic clipboard were visible, but focused-source Ctrl+S did not persist the path within 10s. This is **not a B28 pointer Undo FAIL or PASS**; no held click/drag or PDF occurred. Artifact [13069461275](https://github.com/i-shutov/tikzarium/actions/runs/41271124460/artifacts/13069461275), ZIP SHA-256 `b07e24ca1f8b66d4a90d7e86a873c387ff8f3d8dbfe250abd78dd11e3f549b24`, 7 CRC-valid members and 5/5 embedded report hashes checked; startup, path typed and failure screenshots reviewed. The initial Ctrl+S is now **measured separately** and explicitly recorded as NOT_ACCEPTED if it fails; QA then uses the visible GUI Save button (same required vertical as the rest of the script), waits for and byte-checks the file. This is not a silent PASS of the shortcut. No production source edits or artificial source-file writes. Rerun is NOT_RUN until new exact-head CI and artifact audit. + +## I31 — exact B29 GUI re-acceptance (pending new workflow evidence) + +2026-11-01 · Accepted source truth: main `529217b89e83ffb93393eefb99a2a6ff07967c13`, B-owned draft PR #69 exact `133cab9eff2da0e2ab2ae9574e5328f2dad3ee62`, app source Git subtree `d7a487e91614c7b607ce92f53275e10e2a5ee911`. Previous B28 actual Linux/X11 [41271432298](https://github.com/i-shutov/tikzarium/actions/runs/41271432298) **FAIL**: source editor reintroduced line after app Undo reported success. B29 separates native TextEdit undo snapshots from revision-checked visual Undo; hosted B29 unit/event/TeX/MiKTeX checks passed, but the real desktop acceptance remains **NOT_RUN** until this independently pinned QA branch runs. + +Preserve the entire I29 X11 procedure without weakening predicates: real held-click **and** held-drag Ctrl+Z; GUI Save of byte-identical original Cyrillic/macro source; no ghost command; fresh gesture still works; all six native tools; F1; external GUI XeLaTeX PDF + Unicode/embedded-font review; fit/resize screenshots. An automated workflow success is **not** manual visual acceptance. QA changes only QA workflow/driver/fixture/tests/docs, with a two-parent integration commit from QA and B29 source; do not edit B-owned production or legacy #48. Inspect exact-head artifacts before any QA→B29 or B29→main merge; use expected_head_sha and fresh post-merge CI. Physical Win10/MiKTeX, Mac M1/M4 XeLaTeX **NOT_RUN**; Cargo.lock untracked; no release. + +QA status at commit creation: B29 X11 `NOT_RUN` until actual hosted CI starts and artifacts reviewed; earlier B28 failure remains valid evidence. + +## I33 B30 exact tree integration checkpoint + +2026-11-01 · B30 #69 `750fb6b743a516f44a362f2f568ce1b92c69e6f4`, app tree `965933b3f70504f2f4082bc22795bab67fad5d44`, main `529217b89e83ffb93393eefb99a2a6ff07967c13`; QA-only source/app exact pin, real Linux/X11 held Undo, subsequent fresh gesture, six native tool generation, F1 and PDF/resize captures are prescribed. B29 X11 [41274957152](https://github.com/i-shutov/tikzarium/actions/runs/41274957152) is a verified FAIL, never a B30 PASS. Hosted B30 Rust/D QA/N1/MiKTeX/fonts all SUCCESS; obsolete I23 B26-pinned workflow fails preflight, not B30 GUI. Actual B30 X11, independent report/source/PDF/PNG inspection and physical Win10/M1/M4 acceptance NOT_RUN on commit creation. No merge or release authorized by this note. + +### I33 initial offline predicate issue — corrected before GUI + +[First B30 QA run 41278805656](https://github.com/i-shutov/tikzarium/actions/runs/41278805656) **FAIL before cargo build/GUI** because renamed B30 fixture marker `b30_release_source_history_regression` did not match old unit-test lookup `b30_editor_undo_history_regression`. This is QA-only fixture/test mismatch, not a B30 GUI regression. Corrected test lookup to exact fixture field without weakening any source/gesture predicates; eight offline predicates require rerun, real B30 X11 remains NOT_RUN until new verified executable run and artifacts. + +### I34 XQueryPointer diagnostic gate (after real B30 X11 FAIL) + +[Exact I33 real Linux/X11 41279059557](https://github.com/i-shutov/tikzarium/actions/runs/41279059557) **FAIL** at held click Ctrl+Z, after 8/8 offline QA predicates and actual app build PASS. Artifact [13072965209](https://github.com/i-shutov/tikzarium/actions/runs/41279059557/artifacts/13072965209), ZIP SHA-256 `43aaffdc28bb7109c58d16ba35fffa9ec0e11e3441ee9e29b8f1efbe91b709e9`, 13 CRC-valid, 11/11 report hashes, screenshots independently reviewed. B30 screenshot after Ctrl+Z shows Line #2 before explicit driver mouseup; GUI Save contains IDs 1 and 2, saved .tex SHA `cb0581fccf2003b0e2b15c4e63ff267cadbaee86f36c256f8ed15c78a3a2e39e`, exactly same as B29 failure. **Server Button1Mask was not yet verified** across independent xdotool clients, so attribution is unresolved. I34 QA-only standard-library ctypes Xlib XQueryPointer checks Button1Mask before AND after Ctrl+Z, appends mask/phase to evidence, fails closed if not genuinely held, and preserves byte-exact rollback and no-ghost assertions. This avoids confusing XTest releasing the button with a true production defect. Fresh I34 real X11 run/verified artifact NOT_RUN at this commit; B production unchanged; no merge, physical Win10/M1/M4 NOT_RUN. + +## I35 verified QA holding defect and XTest fix + +Independent [I34 X11 41279540150](https://github.com/i-shutov/tikzarium/actions/runs/41279540150) **failed closed with actual server button state**: Button1Mask 256 before Ctrl+Z, 0 after `xdotool key --clearmodifiers ctrl+z`, both measurements before script mouseup. ZIP [13073180399](https://github.com/i-shutov/tikzarium/actions/runs/41279540150/artifacts/13073180399), SHA-256 `1f597a892e5bda878c0b84cfec78f0858e7e89138a55591b685721c4f807bc7a`, CRC-clean and 11/11 report hashes. Earlier I32/I33 X11 “held” ghost evidence is **INVALID_HELD_INPUT**, not accepted B30 root-cause proof. Native libXtst XTestFakeKeyEvent `Control_L + z` key-only sequence verified in local isolated Xvfb with server Button1Mask staying at 256 before/after, then 0 on independent button up. I35 QA-only driver now uses direct XTest for the held Undo and retains both XQueryPointer guards; negative source rollback still byte-for-byte exact. Actual I35 GUI/TeX/PDF/PNG acceptance NOT_RUN until rerun. B30 production and #48 untouched, physical Win10/M1/M4 NOT_RUN; no release. + +## I36 keyboard receipt forensic enhancement after I35 + +Real I35 [41279880858](https://github.com/i-shutov/tikzarium/actions/runs/41279880858) actual X11 GUI **FAIL** with Button1Mask 256 both before/after XTest Ctrl+Z, and two owned lines after release, even though no app.log event flagged. This proves the pointer was held but not that the application received a usefully timed Ctrl+Z keypress. The I36 QA-only driver XSyncs every XTest Control_L/z press/release, dwells 80ms at each phase and verifies true server `XQueryKeymap` key-down bit, while enforcing Button1Mask held at every phase and saving focused X11 window id. Any loss fails closed before source attribution; byte-for-byte user TeX/no-ghost constraints untouched. Local Xvfb verification confirmed Control_L/z server key bits and Button1Mask throughout, then Button1=0 only after explicit mouseup. CI and independently reviewed real GUI evidence I36 NOT_RUN until workflow completes; physical Win10/Mac NOT_RUN. diff --git a/plans/2026-11-01_I26_B27_HELD_UNDO_REAL_X11.md b/plans/2026-11-01_I26_B27_HELD_UNDO_REAL_X11.md new file mode 100644 index 00000000..db099a2d --- /dev/null +++ b/plans/2026-11-01_I26_B27_HELD_UNDO_REAL_X11.md @@ -0,0 +1,37 @@ +# I26 — B27 actual keyboard Undo while holding the native TikZ pointer +2026-11-01 · QA integration, only `i-shutov/tikzarium`. + +## User vertical and isolation +B27 #69 fixes a source-revision-safe Undo that formerly allowed a held guide gesture to create a ghost TikZ command after the mouse release. Do not accept only egui RawInput/UNIT_EVENT tests. Branch from exact B27 `1805a46cbca2985c4b4798ccf4b0e22171107297`, preserve production ownership B and legacy active QA #48 untouched, and integrate QA into the current B branch only after exact SHA and evidence review. + +## Hosted acceptance +1. Verify the tested exact checkout and B27 app source tree; run isolated Linux/X11 real process, external XeLaTeX, no hidden installer. +2. Paste original Cyrillic and opaque user macro through GUI, Save, create first native line with genuine xdotool drag. +3. For held click *and* held drag: press the native guide, send Ctrl+Z while still held, release, GUI Save, demand full exact original source bytes, zero owned lines, independent screenshots; then intentionally draw a fresh line and verify source persistence. A failed source Undo or a ghost insertion is FAIL. +4. Continue all six native commands with GUI Save, actual GUI Build/Export, PDF Unicode/font checks, Russian F1 and full-page/resize screenshots. Inspect uploaded PDF/PNG and SHA manifest manually; mere frame difference or bootstrap screenshot is not visual PASS. +5. After independent exact-head hosted CI acceptance, lease-merge QA into B with expected_head_sha, rerun B exact-head workflows and independently review critical artifacts, then B→main and separate main post-merge. No premature release. + +## Evidence boundary +Real hosted Linux X11 != physical Win10/MiKTeX or Mac ARM external XeLaTeX. Egui unit event != desktop process. Physical tests NOT_RUN; Cargo.lock release reproducibility NOT_VERIFIED. Do not call a workflow dispatched a completed result. + +## I28 B28 exact-source integration + +2026-11-01 · I28 QA integration onto exact B28 `f2dfbd0bb073e6152146a151eca138af8fc030e1` (independently verified app src tree `4be7a5aebbdd77a820abc08ea37d349dcab6bcec`). Real B27 X11 [41267001529](https://github.com/i-shutov/tikzarium/actions/runs/41267001529) was FAIL and remains historical evidence. B28 routes Undo during an armed guide pointer despite stale focused TextEdit, consuming the key; unweakened genuine X11 held-click/held-drag, exact saved-source rollback, fresh gesture, 6 native tools, F1 and GUI Save/Build/Export PDF must pass with independent source/PDF/screenshot review. Unit-event B28 success alone is insufficient. Real B28 X11 NOT_RUN, physical Win10/MiKTeX and Mac M1/M4 NOT_RUN, Cargo.lock untracked; no release. + +## I31 — exact B29 GUI re-acceptance (pending new workflow evidence) + +2026-11-01 · Accepted source truth: main `529217b89e83ffb93393eefb99a2a6ff07967c13`, B-owned draft PR #69 exact `133cab9eff2da0e2ab2ae9574e5328f2dad3ee62`, app source Git subtree `d7a487e91614c7b607ce92f53275e10e2a5ee911`. Previous B28 actual Linux/X11 [41271432298](https://github.com/i-shutov/tikzarium/actions/runs/41271432298) **FAIL**: source editor reintroduced line after app Undo reported success. B29 separates native TextEdit undo snapshots from revision-checked visual Undo; hosted B29 unit/event/TeX/MiKTeX checks passed, but the real desktop acceptance remains **NOT_RUN** until this independently pinned QA branch runs. + +Preserve the entire I29 X11 procedure without weakening predicates: real held-click **and** held-drag Ctrl+Z; GUI Save of byte-identical original Cyrillic/macro source; no ghost command; fresh gesture still works; all six native tools; F1; external GUI XeLaTeX PDF + Unicode/embedded-font review; fit/resize screenshots. An automated workflow success is **not** manual visual acceptance. QA changes only QA workflow/driver/fixture/tests/docs, with a two-parent integration commit from QA and B29 source; do not edit B-owned production or legacy #48. Inspect exact-head artifacts before any QA→B29 or B29→main merge; use expected_head_sha and fresh post-merge CI. Physical Win10/MiKTeX, Mac M1/M4 XeLaTeX **NOT_RUN**; Cargo.lock untracked; no release. + +## I33 — B30 real GUI re-acceptance + +2026-11-01: main `529217b89e83ffb93393eefb99a2a6ff07967c13`; exact B30 production HEAD `750fb6b743a516f44a362f2f568ce1b92c69e6f4`; app source tree `965933b3f70504f2f4082bc22795bab67fad5d44`. B29 real Linux/X11 [41274957152](https://github.com/i-shutov/tikzarium/actions/runs/41274957152) FAIL: second ghost Line existed before explicit mouse-up. B30 release/history fence has 5/5 hosted workflow families SUCCESS, but its real GUI was NOT_RUN when this checkpoint was created. The I33 X11 gate retains held-click/held-drag Ctrl+Z, byte-exact original Cyrillic+macro restoration and zero owned commands, subsequent intentional gesture, six native C tools, Russian F1, GUI Save/Build/Export through external XeLaTeX, complete PDF, responsive fit screenshots, independent TeX/PDF/PNG/manual audit. No B production, old #48 or shared catalogue edits. Merge only after exact head and artifacts, then separate main post-merge CI. Physical Win10/MiKTeX and Mac M1/M4 XeLaTeX NOT_RUN; Cargo.lock not tracked; no release. + +## I35 — hold X11 Button1 through genuine Ctrl+Z + +2026-11-01 · [I34 live run 41279540150](https://github.com/i-shutov/tikzarium/actions/runs/41279540150) **FAIL-CLOSED QA_HARNESS**: native XQueryPointer Button1Mask was 256 before `xdotool key --clearmodifiers ctrl+z` and 0 immediately afterward, **before** the driver's explicit mouseup. Thus earlier B29/B30 ghost screenshots do not prove a production failure in a genuinely held-button scenario. ZIP artifact [13073180399](https://github.com/i-shutov/tikzarium/actions/runs/41279540150/artifacts/13073180399), CRC-clean 13 files/11 embedded hashes, trace inspected. I35 replaces only that held-key event with native Xlib/XTest `Control_L down, z down/up, Control_L up`. Verified under isolated local Xvfb that direct XTest preserves Button1Mask=256 throughout key sequence, and Button1 becomes 0 only after explicit button release. Keep XQueryPointer guards both sides, zero-ghost/byte-exact Unicode original, six native authoring tools, Russian F1, GUI PDF and independent screenshot review; no production edit, no merge until exact live head CI+visual proof; physical Win10/Mac NOT_RUN. + +## I36 — verify actual Ctrl+Z key delivery across server-held Button1 + +2026-11-01 · I35 live [41279880858](https://github.com/i-shutov/tikzarium/actions/runs/41279880858) correctly proved Button1Mask **256 before AND after** native XTest Ctrl+Z, unlike invalid I33; yet original Line was not undone and release inserted ghost #2. Source SHA `cb0581fccf2003b0e2b15c4e63ff267cadbaee86f36c256f8ed15c78a3a2e39e` in CRC-checked [artifact 13072791651](https://github.com/i-shutov/tikzarium/actions/runs/41279880858/artifacts/13072791651), SHA256 `70e6f332bc48289d90eee88a50c457833b86592f2d35bea9ceb13906e26c05eb`, 11/11 report hashes, screenshot manually reviewed. However fast XTest key down/up may never be observed as a discrete egui shortcut event; app.log empty and no independent keyboard receipt. I36 holds each XTest keyboard state for 80ms with XSync, verifies XQueryKeymap reports requested Ctrl/z down and up at every step, and checks XQueryPointer Button1Mask never clears. Records X11 input-focus window and key events in immutable report, then preserves the original exact-source/no-ghost predicate and subsequent six tools/F1/GUI XeLaTeX-PDF/resize acceptance. Offline test covers 256-bit keymap bit extraction. All QA-only, B production untouched; no merge without exact live accepted report/artifact, physical Win10/M1/M4 NOT_RUN. diff --git a/scripts/qa_n1_desktop.py b/scripts/qa_n1_desktop.py index c6d54c31..3d0c4600 100644 --- a/scripts/qa_n1_desktop.py +++ b/scripts/qa_n1_desktop.py @@ -8,6 +8,7 @@ from __future__ import annotations import argparse +import ctypes import hashlib import json import os @@ -33,6 +34,131 @@ """ +def x11_button1_mask() -> int: + """Query X11 server Button1Mask (1<<8), not the QA driver's intent. + + Repeated xdotool commands are independent XTest clients. A screenshot + taken before the driver's mouseup does not prove X11 still sees Button1 + pressed. Fail closed when XQueryPointer cannot establish the live mask. + """ + x11 = ctypes.CDLL("libX11.so.6") + ptr = ctypes.c_void_p + window = ctypes.c_ulong + integer = ctypes.c_int + mask_type = ctypes.c_uint + x11.XOpenDisplay.argtypes = [ctypes.c_char_p] + x11.XOpenDisplay.restype = ptr + x11.XDefaultScreen.argtypes = [ptr] + x11.XDefaultScreen.restype = integer + x11.XRootWindow.argtypes = [ptr, integer] + x11.XRootWindow.restype = window + x11.XQueryPointer.argtypes = [ + ptr, window, ctypes.POINTER(window), ctypes.POINTER(window), + ctypes.POINTER(integer), ctypes.POINTER(integer), + ctypes.POINTER(integer), ctypes.POINTER(integer), + ctypes.POINTER(mask_type), + ] + x11.XQueryPointer.restype = integer + x11.XCloseDisplay.argtypes = [ptr] + x11.XCloseDisplay.restype = integer + display = x11.XOpenDisplay(None) + if not display: + raise RuntimeError("XQueryPointer cannot open active X11 DISPLAY") + try: + root = x11.XRootWindow(display, x11.XDefaultScreen(display)) + root_reply, child = window(), window() + root_x, root_y, win_x, win_y = integer(), integer(), integer(), integer() + mask = mask_type() + if not x11.XQueryPointer( + display, root, ctypes.byref(root_reply), ctypes.byref(child), + ctypes.byref(root_x), ctypes.byref(root_y), + ctypes.byref(win_x), ctypes.byref(win_y), ctypes.byref(mask), + ): + raise RuntimeError("XQueryPointer did not return a pointer mask") + return mask.value + finally: + x11.XCloseDisplay(display) + + +def x11_button1_pressed(mask: int) -> bool: + return bool(mask & (1 << 8)) + + +def xtest_ctrl_z_sequence(ctrl_keycode: int, z_keycode: int) -> tuple[tuple[int, bool], ...]: + """Physical XTest key sequence; never sends a mouse button release.""" + return ((ctrl_keycode, True), (z_keycode, True), + (z_keycode, False), (ctrl_keycode, False)) + + +def x11_keycode_down(keymap: bytes, keycode: int) -> bool: + """The XQueryKeymap 256-bit server state, independent of app focus.""" + if len(keymap) != 32 or not 8 <= keycode <= 255: + raise ValueError("X11 keymap must be 32 bytes with a valid keycode") + return bool(keymap[keycode // 8] & (1 << (keycode % 8))) + + +def xtest_ctrl_z_preserving_pointer() -> dict: + """Send X11 Ctrl+Z with physically observable key-down dwell. + + A burst of four XTest key events can elide the hotkey from the GUI + event-loop without ever releasing the mouse. Each event is XSync'd, + its XQueryKeymap state verified, and held for > one typical GUI frame. + This is a QA-only server-side keyboard input, not a Session call. + """ + x11 = ctypes.CDLL("libX11.so.6") + xtest = ctypes.CDLL("libXtst.so.6") + ptr = ctypes.c_void_p + x11.XOpenDisplay.argtypes = [ctypes.c_char_p] + x11.XOpenDisplay.restype = ptr + x11.XKeysymToKeycode.argtypes = [ptr, ctypes.c_ulong] + x11.XKeysymToKeycode.restype = ctypes.c_ubyte + x11.XSync.argtypes = [ptr, ctypes.c_int] + x11.XSync.restype = ctypes.c_int + x11.XQueryKeymap.argtypes = [ptr, ptr] + x11.XQueryKeymap.restype = ctypes.c_int + x11.XGetInputFocus.argtypes = [ + ptr, ctypes.POINTER(ctypes.c_ulong), ctypes.POINTER(ctypes.c_int)] + x11.XGetInputFocus.restype = ctypes.c_int + x11.XCloseDisplay.argtypes = [ptr] + x11.XCloseDisplay.restype = ctypes.c_int + xtest.XTestFakeKeyEvent.argtypes = [ + ptr, ctypes.c_uint, ctypes.c_int, ctypes.c_ulong] + xtest.XTestFakeKeyEvent.restype = ctypes.c_int + display = x11.XOpenDisplay(None) + if not display: + raise RuntimeError("XTest Ctrl+Z cannot open active X11 DISPLAY") + try: + ctrl = x11.XKeysymToKeycode(display, 0xFFE3) # XK_Control_L + z = x11.XKeysymToKeycode(display, ord("z")) + if not ctrl or not z or ctrl == z: + raise RuntimeError("XTest Ctrl+Z keycodes unavailable") + focused = ctypes.c_ulong() + revert_to = ctypes.c_int() + x11.XGetInputFocus(display, ctypes.byref(focused), ctypes.byref(revert_to)) + trace = {"focus_window": focused.value, "key_events": []} + for code, pressed in xtest_ctrl_z_sequence(ctrl, z): + if not xtest.XTestFakeKeyEvent(display, code, int(pressed), 0): + raise RuntimeError("XTestFakeKeyEvent rejected Ctrl+Z") + x11.XSync(display, 0) + keymap = ctypes.create_string_buffer(32) + x11.XQueryKeymap(display, keymap) + actual = x11_keycode_down(keymap.raw, code) + button_mask = x11_button1_mask() + trace["key_events"].append({ + "keycode": code, "expected_pressed": pressed, + "server_pressed": actual, "button1_mask": button_mask, + "button1": x11_button1_pressed(button_mask), + }) + if actual != pressed: + raise RuntimeError("X11 server failed to register Ctrl+Z key state") + if not x11_button1_pressed(button_mask): + raise RuntimeError("X11 server dropped held Button1 during Ctrl+Z") + time.sleep(0.08) + return trace + finally: + x11.XCloseDisplay(display) + + def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() @@ -107,6 +233,16 @@ def original_preserved(original: bytes, current: bytes) -> bool: return b"".join(remainder) == original +def verify_held_undo(before: bytes, after: bytes, original: bytes) -> None: + """Both source rollback and non-insertion of a held gesture are required.""" + if len(owners(before)) != 1 or owners(before)[0]["tool"] != "line": + raise RuntimeError("B30 setup must contain exactly the original line") + if after != original: + raise RuntimeError("B30 Undo/mouse-up did not restore exact source bytes or inserted a ghost") + if owners(after): + raise RuntimeError("B30 cancelled gesture left an owned native command") + + def wait_for(predicate, seconds: float, message: str) -> None: deadline = time.monotonic() + seconds while time.monotonic() < deadline: @@ -142,6 +278,8 @@ def main() -> int: "pointer_authoring": "NOT_RUN", "manual_visual_review": "NOT_RUN", "gui_pdf_export": "NOT_RUN", "fit_width": "NOT_RUN", "full_page": "NOT_RUN", "fit_page": "NOT_RUN", "responsive_fit": "NOT_RUN", + "b30_held_pointer_undo": "NOT_RUN", + "x11_button1_mask_trace": [], "f1_help_visual": "NOT_RUN", "reviewed_app_tree_sha": os.getenv("TIKZARIUM_APP_TREE_SHA", "UNKNOWN"), "steps": []} @@ -206,11 +344,25 @@ def key(value: str) -> None: # focus. Leave the path editor explicitly before triggering actual # global Save; never synthesize a file from this test driver. click(profile["source_click"]) + # Observe the Ctrl+S path, but do not let a flaky focused-TextEdit + # shortcut conceal the separate user-required *GUI Save* vertical. + # Explicitly record the shortcut as unverified when the file was not + # persisted; never fabricate the TeX document in QA. key("ctrl+s") - wait_for(source.is_file, 10, "Ctrl+S after source focus did not save the entered path") + try: + wait_for(source.is_file, 2.0, + "Ctrl+S after source focus did not persist entered path") + report["keyboard_save_shortcut"] = "PASS_REAL_X11" + except RuntimeError: + report["keyboard_save_shortcut"] = "DID_NOT_PERSIST_IN_2S_NOT_ACCEPTED" + screenshot("02b-before-explicit-gui-save.png") + click(profile["save_click"]) + wait_for(source.is_file, 10, + "Visible GUI Save button did not persist entered path") if source.read_bytes() != FIXTURE.encode("utf-8"): raise RuntimeError("Saved buffer differs from actual pasted Unicode source") - report["steps"].append({"case": "keyboard_paste_save", "status": "PASS", + report["steps"].append({"case": "gui_paste_save", "status": "PASS", + "keyboard_save": report["keyboard_save_shortcut"], "sha256": sha256(source)}) screenshot("02-saved-source.png") for case in profile.get("gestures", []): @@ -252,6 +404,76 @@ def key(value: str) -> None: "sha256": sha256(source), "id": found[-1]["id"], "save_attempts": save_attempts}) screenshot("pointer-" + case["tool"] + ".png") + if case["tool"] == "line" and profile.get("undo_held_pointer"): + # B30: actual X11 keyboard Undo WHILE the pointer remains held + # over the guide; then a real mouse-up, Save, and source-byte + # inspection. Never fabricate the restored TeX file in QA. + for moved in (False, True): + prior = source.read_bytes() + verify_seed = owners(prior) + if len(verify_seed) != 1 or verify_seed[0]["tool"] != "line": + raise RuntimeError("B30 expected one persisted line before Ctrl+Z") + click(case["tool_click"]) + run(["xdotool", "mousemove", "--sync", "--window", window, + str(start[0]), str(start[1]), "mousedown", "1"]) + if moved: + time.sleep(0.2) + run(["xdotool", "mousemove", "--sync", "--window", window, + str(end[0]), str(end[1])]) + time.sleep(0.25) + variant = "drag" if moved else "click" + mask_before = x11_button1_mask() + report["x11_button1_mask_trace"].append({ + "case": variant, "phase": "before_ctrl_z", + "mask": mask_before, "button1": x11_button1_pressed(mask_before)}) + screenshot("b30-" + variant + "-held.png") + if not x11_button1_pressed(mask_before): + raise RuntimeError("QA XTest Button1 was UP before Ctrl+Z; held-pointer scenario not established") + report["x11_held_undo_key_events"] = xtest_ctrl_z_preserving_pointer() + report["x11_held_undo_key_input"] = "XTestFakeKeyEvent Control_L+z, each event XSync/80ms XQueryKeymap" + time.sleep(0.3) + mask_after = x11_button1_mask() + report["x11_button1_mask_trace"].append({ + "case": variant, "phase": "after_ctrl_z_before_explicit_mouseup", + "mask": mask_after, "button1": x11_button1_pressed(mask_after)}) + screenshot("b30-" + variant + "-undo-held.png") + if not x11_button1_pressed(mask_after): + raise RuntimeError("QA XTest Button1 was released before explicit mouseup; cannot attribute ghost to production") + run(["xdotool", "mouseup", "1"]) + time.sleep(0.3) + click(profile["save_click"]) + wait_for(lambda: source.read_bytes() != prior, 10, + "B30 Ctrl+Z did not undo saved first line") + reverted = source.read_bytes() + verify_held_undo(prior, reverted, FIXTURE.encode("utf-8")) + screenshot("b30-" + ("drag" if moved else "click") + "-after-release.png") + report["steps"].append({ + "case": "b30_undo_held_" + ("drag" if moved else "click"), + "status": "PASS_REAL_X11_UNDO_RELEASE_NO_GHOST", + "sha256": sha256(source)}) + # Continue the original six-primitive authoring vertical: + # one fresh intentional line must still work after Undo. + click(case["tool_click"]) + run(["xdotool", "mousemove", "--sync", "--window", window, + str(start[0]), str(start[1]), "mousedown", "1"]) + time.sleep(0.2) + run(["xdotool", "mousemove", "--sync", "--window", window, + str(end[0]), str(end[1])]) + time.sleep(0.2) + run(["xdotool", "mouseup", "1"]) + time.sleep(0.3) + click(profile["save_click"]) + wait_for(lambda: len(owners(source.read_bytes())) == 1, 10, + "B30 fresh guide gesture after Undo did not save") + restored = source.read_bytes() + if (owners(restored)[0]["tool"] != "line" + or not original_preserved(FIXTURE.encode("utf-8"), restored)): + raise RuntimeError("B30 fresh gesture damaged unrelated TikZ") + screenshot("b30-" + ("drag" if moved else "click") + "-fresh-line.png") + report["steps"].append({ + "case": "b30_fresh_after_undo_" + ("drag" if moved else "click"), + "status": "PASS_REAL_X11_FRESH_GESTURE", "sha256": sha256(source)}) + report["b30_held_pointer_undo"] = "PASS_REAL_X11_CLICK_AND_DRAG" if profile.get("gestures"): names = [case["tool"] for case in profile["gestures"]] if len(names) != len(TOOLS) or set(names) != TOOLS: diff --git a/scripts/tests/test_qa_n1_desktop.py b/scripts/tests/test_qa_n1_desktop.py index 9e05f995..c6a3d842 100644 --- a/scripts/tests/test_qa_n1_desktop.py +++ b/scripts/tests/test_qa_n1_desktop.py @@ -43,11 +43,37 @@ def test_malformed_unknown_and_invalid_types_fail(self): with self.assertRaises((ValueError, TypeError, json.JSONDecodeError)): desktop.owners(candidate) - def test_b26_candidate_profile_is_exact_source_pinned(self): + def test_x11_query_keymap_bit_state(self): + bits = bytearray(32) + for code in (37, 52): + bits[code // 8] |= (1 << (code % 8)) + self.assertTrue(desktop.x11_keycode_down(bytes(bits), 37)) + self.assertTrue(desktop.x11_keycode_down(bytes(bits), 52)) + self.assertFalse(desktop.x11_keycode_down(bytes(bits), 53)) + for keymap, code in ((b"", 37), (bytes(32), 7), (bytes(32), 256)): + with self.subTest(code=code): + with self.assertRaises(ValueError): + desktop.x11_keycode_down(keymap, code) + + def test_xtest_ctrl_z_has_only_key_events_and_balanced_releases(self): + self.assertEqual(desktop.xtest_ctrl_z_sequence(37, 52), + ((37, True), (52, True), (52, False), (37, False))) + self.assertNotIn(0, [code for code, _ in desktop.xtest_ctrl_z_sequence(37, 52)]) + + def test_x11_button1_mask_detection_is_fail_closed(self): + self.assertFalse(desktop.x11_button1_pressed(0)) + self.assertFalse(desktop.x11_button1_pressed(1 << 9)) + self.assertTrue(desktop.x11_button1_pressed(1 << 8)) + self.assertTrue(desktop.x11_button1_pressed((1 << 8) | (1 << 2))) + + def test_b30_source_history_profile_is_exact_source_pinned(self): profile_path = Path(__file__).resolve().parents[2] / "fixtures/qa/n1-desktop-x11-1366.json" profile = json.loads(profile_path.read_text(encoding="utf-8")) self.assertEqual(profile["reviewed_app_tree_sha"], - "47c4c86be10471a5f56f7019ad63a77e7f4480f5") + "965933b3f70504f2f4082bc22795bab67fad5d44") + self.assertTrue(profile["undo_held_pointer"]) + self.assertTrue(profile["b30_release_source_history_regression"]) + self.assertNotIn("b28_focused_textedit_regression", profile) self.assertNotIn("reviewed_app_source", profile) self.assertEqual(profile["fit_click"], [1027, 72]) self.assertEqual(profile["zoom_out_click"], [882, 72]) @@ -59,6 +85,19 @@ def test_b26_candidate_profile_is_exact_source_pinned(self): self.assertEqual(len(profile["gestures"]), 6) self.assertEqual({case["tool"] for case in profile["gestures"]}, desktop.TOOLS) + def test_b27_real_undo_predicate_rejects_ghost_and_unrelated_mutation(self): + original = desktop.FIXTURE.encode("utf-8") + before = original.replace(b"\\end{tikzpicture}", + line(version=2) + b"\\end{tikzpicture}") + desktop.verify_held_undo(before, original, original) + for bad in (before, before + line(2, version=2), + original.replace("Не менять".encode(), b"changed")): + with self.subTest(bad=bad): + with self.assertRaises(RuntimeError): + desktop.verify_held_undo(before, bad, original) + with self.assertRaises(RuntimeError): + desktop.verify_held_undo(original, original, original) + def test_generated_lines_preserve_unicode_original(self): original = desktop.FIXTURE.encode() current = original.replace(b"\\end{tikzpicture}",