diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 20111c15..b33f8c4b 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -195,3 +195,41 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, FIXED (2026-06-05): root cause confirmed by probe at 3440px width — the bottom bar used Row[ ...left (incl. a Flexible flex:1 loose item, the Claude status marquee), Spacer(), ...right ]. The Spacer (Expanded, flex:1) and the left flex:1 item SPLIT the free space 50/50, so the Spacer only pushed the right group by HALF the free space. That drift is proportional to width: tiny at 1200px (why probes/normal screens looked fine), ~1500px at 3440px (the right group floated to mid-bar) — hence ''only on ultrawide''. Probe: OLD R.right=1934 at a 3440 edge; NEW R.right=3440. FIX (app.dart StatusbarHost): explicit two-column layout — left group wrapped in Expanded(Row[...]) so it absorbs ALL free space (flex item flexes within it), right group trails at intrinsic width → hugs the workspace block''s right edge by construction, width-independent. Regression test test/app_statusbar_test.dart asserts the right group at width-8 for BOTH 600px and 3440px. Full fast suite green.', NULL, '2026-06-05 10:02:12', '2026-06-05 10:02:12', '2026-06-05 10:02:12', NULL, 'a3934166a2af5b191cbf0670c93468f2', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-239', 'status', 'in_progress', 'done', NULL, '2026-06-05 10:02:12', '2026-06-05 10:02:12', '2026-06-05 10:02:12', NULL, '00b3cc01164f91ebb5017ca4bc778d3e', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'status', 'backlog', 'ready', NULL, '2026-06-05 11:04:12', '2026-06-05 11:04:12', '2026-06-05 11:04:12', NULL, '31ea36a8d7de4a25e9ed0f988f1b0437', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-50', 'status', 'backlog', 'ready', NULL, '2026-06-05 11:04:25', '2026-06-05 11:04:25', '2026-06-05 11:04:25', NULL, '0b3392508bac401f96d1b2803f378855', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'status', 'backlog', 'ready', NULL, '2026-06-05 12:15:59', '2026-06-05 12:15:59', '2026-06-05 12:15:59', NULL, 'bdd6d1b4412ae4b7056adedd6282734a', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'status', 'ready', 'in_progress', NULL, '2026-06-05 13:07:20', '2026-06-05 13:07:20', '2026-06-05 13:07:20', NULL, 'fe442bfcab62c754cfa229bfde19b026', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'description', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases). + +ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', NULL, '2026-06-05 13:08:02', '2026-06-05 13:08:02', '2026-06-05 13:08:02', NULL, 'cc247a7da845b921a5e1864a0cb5c9cc', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'status', 'in_progress', 'done', NULL, '2026-06-05 13:08:05', '2026-06-05 13:08:05', '2026-06-05 13:08:05', NULL, '18577271ba7a5602ab1229cc1c97281b', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index f8a9e9e6..d3e02f05 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -502,3 +502,74 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, FIXED (2026-06-05): root cause confirmed by probe at 3440px width — the bottom bar used Row[ ...left (incl. a Flexible flex:1 loose item, the Claude status marquee), Spacer(), ...right ]. The Spacer (Expanded, flex:1) and the left flex:1 item SPLIT the free space 50/50, so the Spacer only pushed the right group by HALF the free space. That drift is proportional to width: tiny at 1200px (why probes/normal screens looked fine), ~1500px at 3440px (the right group floated to mid-bar) — hence ''only on ultrawide''. Probe: OLD R.right=1934 at a 3440 edge; NEW R.right=3440. FIX (app.dart StatusbarHost): explicit two-column layout — left group wrapped in Expanded(Row[...]) so it absorbs ALL free space (flex item flexes within it), right group trails at intrinsic width → hugs the workspace block''s right edge by construction, width-independent. Regression test test/app_statusbar_test.dart asserts the right group at width-8 for BOTH 600px and 3440px. Full fast suite green.', 'done', 'medium', NULL, NULL, 'D-6', '2026-06-03 21:41:58', '2026-06-05 10:02:12', NULL, 'ed1b3328a85f75c11f90df13cd667995', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-241', 'task', NULL, 'Audit widget tests for ultrawide coverage (IDE runs wide)', 'Lesson from T-239: a width-PROPORTIONAL layout bug (status-bar right group drifting to mid-bar via a Spacer-vs-flex split) was invisible at the default 800px flutter_test surface and at 1200px, but glaring at 3440px. clide is an IDE — ultrawide (3440, 5120, ultrawide+vertical splits) is a VERY common real screen size, so width-sensitive layout that only ever gets tested at 800px hides exactly this class of bug.\n\nScope: review the widget/golden suite and add ultrawide coverage where layout is width-sensitive. Candidates: the bottom status bar (done — test/app_statusbar_test.dart now covers 600 + 3440), panel/slot layout + drag-resize (lib/app.dart RootLayout, SlotHost), tab strips (overflow/scroll at wide), ClideMarquee (T-160 — only tested narrow; also check it doesn''t mis-behave wide), conversation view / activity card, the command palette + quick-open overlays (max-width/centering on wide), modal pickers, status items.\n\nApproach: (1) add a shared test helper to pump at a given surface width via tester.view.physicalSize (note: a wide SizedBox under the default 800px surface is CLAMPED — must set view.physicalSize, see test/app_statusbar_test.dart pumpAt). (2) For layout-sensitive widgets, assert key positions/no-overflow at BOTH a normal and an ultrawide width. (3) Don''t blanket-add to every test — target width-sensitive layout (Row/Spacer/Expanded/Flexible/Align, max-width caps, centering). Note any widget that SHOULD cap/center on ultrawide (readability) vs fill. Relates to Q-26 (small-screen layout) — same responsive concern at the other end.', 'backlog', 'medium', NULL, NULL, 'D-23', '2026-06-05 10:02:31', '2026-06-05 10:02:31', NULL, '642d817c1cc2019ac7d7550137b31509', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-242', 'task', NULL, 'Quiet test reporter: use failures-only in ci scripts (only failures + final count)', NULL, 'backlog', 'medium', NULL, NULL, NULL, '2026-06-05 10:17:47', '2026-06-05 10:17:47', NULL, 'fc2704e0a4388b220d44578b7e55e2e8', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-42', 'task', 'T-5', 'Decide diff view placement', 'Diff view currently contributes to workspace. Spec doesn''t place it. Likely belongs in context panel (viewer) or as an editor overlay. Depends on Q-27.', 'ready', 'low', NULL, NULL, 'D-48', '2026-04-22 21:03:40', '2026-06-05 11:04:12', NULL, 'c8f5d40e725215f57ee812d620166de2', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-50', 'story', NULL, 'toast notification system', 'Non-modal toast notifications for operation feedback (git push succeeded, extension activated, update available, errors). Slide in from bottom-right or top-right, auto-dismiss after timeout, manually dismissable. Queue multiple toasts. Severity levels map to status tokens (success/warning/error/info).', 'ready', 'medium', NULL, NULL, NULL, '2026-04-23 20:31:48', '2026-06-05 11:04:25', NULL, 'cc078173e0e617deefe39dfef8176bdf', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'task', 'T-115', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).', 'ready', 'medium', NULL, NULL, NULL, '2026-05-18 09:06:59', '2026-06-05 12:15:59', NULL, '91d9b46dcf8787e88f1dd14ebab7ccfa', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'task', 'T-115', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-18 09:06:59', '2026-06-05 13:07:20', NULL, 'f8a0e5f4e3c2d52b7c28f4359d4663bf', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'task', 'T-115', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases). + +ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-05-18 09:06:59', '2026-06-05 13:08:02', NULL, '87332a80948a452e162826512ee84358', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-122', 'task', 'T-115', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle', 'T-115 follow-up: tap-driven widget test for sticky-startup toggle. + +Adding a tap-driven (or even render-only) test that pumps WelcomeView with a non-empty recents list hangs the Flutter test runner until timeout. + +Investigation 2026-05-22 (narrowed the cause): +- The hang is SYNCHRONOUS: `flutter test --timeout 45s` never fires; only an external `timeout` kills it. So the Dart isolate event loop is blocked, not awaiting. +- Reduced to a minimal repro: pumping two NESTED ClideTappable widgets under the shared test harness (test/helpers/widget_harness.dart) hangs. A SINGLE ClideTappable (as in _ActionRow) renders fine — that is why all existing WelcomeView tests pass (empty recents = no nested tappable). The recents path nests one: _RecentRow is a ClideTappable whose subtree contains _StickyToggle (a Semantics-wrapped ClideTappable). +- The shared harness uses `Overlay(canSizeOverlay: true)` over a zero-size `MediaQuery`, which triggers an intrinsic-sizing pass. Strongly suspect the nested ClideTappable (Focus + MouseRegion + GestureDetector + DecoratedBox stack) diverges under intrinsic dimension computation. +- Workarounds that did NOT help: setting tester.view.physicalSize; wrapping in SizedBox; wrapping in Center+SizedBox; a custom bounded MediaQuery harness (that custom harness hung even on a single tappable — likely its own confound, do not reuse it). +- The prior ClideTooltip/MouseRegion-timer hypothesis is WRONG: the recents widgets pass no tooltip to ClideTappable, so no ClideTooltip is built, and _StickyToggle uses Semantics(tooltip:) (metadata only, no widget/timer). + +Next steps: +- Decide whether this is a real ClideTappable bug (nested tappables would also hang the live app welcome screen with recents) or strictly a canSizeOverlay-intrinsic-sizing test artifact. Check the running app: open welcome with >=1 recent project and confirm it does NOT hang. If the app is fine, the fix is harness-side (give WelcomeView tight constraints so no intrinsic pass), and the shared harness or a welcome-specific harness needs adjusting. If the app DOES hang, ClideTappable has a real nested-layout bug — fix it (likely in lib/widgets/src/clide_tappable.dart intrinsic/layout handling) and this becomes higher priority. +- Once unblocked, this also unblocks lib/builtin/welcome/src/welcome_view.dart coverage (74 uncovered lines, the dominant lib/builtin/ gap toward the T-89 95% target). + +ProjectManager sticky-startup logic is already covered by test/kernel/src/project_test.dart (14 cases). + +ROOT CAUSE FOUND + FIXED (2026-06-05). The strand is NOT nested ClideTappable / intrinsic sizing (that prior hypothesis is disproven: seeding an EMPTY recents list — no rows, no nested tappables — also hangs, while rendering real recent rows seeded correctly passes). Real cause: SettingsStore.set does real file I/O (writeAsString); awaiting settings.set + project.loadRecents INSIDE the testWidgets body runs that I/O in fake-async, which traps the completion callback so the await never returns (a +0 strand only SIGKILL clears). FIX: seed recents via tester.runAsync(() async {...}) (real event loop), then pump in a tight bounded tree (not the shared harness, whose unbounded width is a separate WelcomeView layout hazard). test/builtin/welcome/widget_test.dart now has working recents render + sticky-toggle + open-recent tests; the skip is removed. Harness lesson recorded.', 'done', 'medium', NULL, NULL, NULL, '2026-05-18 09:06:59', '2026-06-05 13:08:05', NULL, 'accb384a88b9bd863b2d0a440a1a2c3d', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/Makefile b/Makefile index 6ccbb280..cc727ad2 100644 --- a/Makefile +++ b/Makefile @@ -310,7 +310,7 @@ decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejec pql decisions validate .PHONY: push-check -push-check: decisions-validate test-core test-coverage coverage-gate changelog-gate ## Pre-push gate (fast — <2 min target). test-coverage already runs the a11y suite (test/a11y), so no separate test-a11y pass. +push-check: decisions-validate changelog-gate test-coverage coverage-gate test-core ## Pre-push gate (fast — <2 min target). Order is fail-fast: instant gates (decisions, changelog) first, then the coverage suite + gate (the expensive, most-likely-to-fail stage) BEFORE test-core — a coverage miss aborts here instead of after running everything, so a fix doesn't force a full re-run of the rest. test-coverage already runs the a11y suite (test/a11y), so no separate test-a11y pass. .PHONY: push-check-full push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower). diff --git a/ci/test.sh b/ci/test.sh index c70fff35..7cf77784 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -15,6 +15,13 @@ set -euo pipefail cd "$(dirname "$0")/.." +# Reporter: failures-only keeps the gate output to failing tests + a final +# pass/fail count, instead of one line per test (the `expanded` reporter the +# runner picks when stdout isn't a TTY — which buries real failures in +# thousands of pass lines). Override with TEST_REPORTER=expanded when +# debugging a specific run. (T-242) +REPORTER="${TEST_REPORTER:-failures-only}" + coverage=0 [[ "${1:-}" == "--coverage" ]] && coverage=1 @@ -28,24 +35,24 @@ echo "==> dart test (pty — unreliable under the flutter test runner; serial)" # --concurrency=1: these spawn real PTYs and compete for fds when run in # parallel, which flaked them (registry/session). Serialize — the proper fix # for resource-bound tests, vs. the old per-test `retry:` band-aid. (T-193) -dart test --concurrency=1 --tags pty test/pty/session_test.dart test/panes/registry_test.dart +dart test -r "$REPORTER" --concurrency=1 --tags pty test/pty/session_test.dart test/panes/registry_test.dart # The parallel pool excludes both pty (runs under dart test, above) and # serial-tagged tests (concurrency-vulnerable — run in their own --concurrency=1 # pass below). See dart_test.yaml + T-193. if [[ "$coverage" == 1 ]]; then echo "==> flutter test --coverage (parallel pool; excludes pty + serial)" - flutter test --coverage --exclude-tags "pty || serial" --timeout 60s + flutter test -r "$REPORTER" --coverage --exclude-tags "pty || serial" --timeout 60s cp coverage/lcov.info coverage/lcov.parallel.info echo "==> flutter test --coverage (serial-tagged; --concurrency=1)" - flutter test --coverage --tags serial --concurrency=1 --timeout 60s + flutter test -r "$REPORTER" --coverage --tags serial --concurrency=1 --timeout 60s echo "==> merge coverage (parallel + serial passes → coverage/lcov.info)" python3 ci/merge_lcov.py coverage/lcov.parallel.info coverage/lcov.info > coverage/lcov.merged.info mv coverage/lcov.merged.info coverage/lcov.info rm -f coverage/lcov.parallel.info else echo "==> flutter test (dev; parallel pool, excludes pty + serial)" - flutter test --exclude-tags "pty || serial" --concurrency=12 --timeout 60s + flutter test -r "$REPORTER" --exclude-tags "pty || serial" --concurrency=12 --timeout 60s echo "==> flutter test (dev; serial-tagged, --concurrency=1)" - flutter test --tags serial --concurrency=1 --timeout 60s + flutter test -r "$REPORTER" --tags serial --concurrency=1 --timeout 60s fi diff --git a/ci/test_a11y.sh b/ci/test_a11y.sh index 2f4bfbb4..517f71be 100755 --- a/ci/test_a11y.sh +++ b/ci/test_a11y.sh @@ -5,4 +5,6 @@ set -euo pipefail cd "$(dirname "$0")/.." echo "==> a11y suite" -flutter test test/a11y/ +# failures-only: failing tests + a final count, not one line per test. +# Override with TEST_REPORTER=expanded when debugging. (T-242) +flutter test -r "${TEST_REPORTER:-failures-only}" test/a11y/ diff --git a/ci/test_core.sh b/ci/test_core.sh index 8064c149..fc8ddef2 100755 --- a/ci/test_core.sh +++ b/ci/test_core.sh @@ -25,6 +25,10 @@ fi # hang. TIMEOUT_SECONDS=${TIMEOUT_SECONDS:-120} +# failures-only: print failing tests + a final count, not one line per test. +# Override with TEST_REPORTER=expanded when debugging. (T-242) +REPORTER="${TEST_REPORTER:-failures-only}" + # Run dart test in its own process group so we can kill descendants on # timeout. `setsid` starts a new session; `timeout --kill-after` SIGKILLs # after SIGTERM if the test ignores it. @@ -32,7 +36,7 @@ CORE_DIRS="test/ipc test/pty test/daemon test/git test/panes test/files test/edi echo "test-core: dart test ${CORE_DIRS} (timeout ${TIMEOUT_SECONDS}s)" if ! timeout --kill-after=5s "${TIMEOUT_SECONDS}s" \ - setsid --wait dart test $CORE_DIRS ; then + setsid --wait dart test -r "$REPORTER" $CORE_DIRS ; then rc=$? if [[ $rc -eq 124 ]]; then echo "test-core: TIMEOUT — killing descendants" >&2