Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fa4ce1bac | ||
|
|
7d951a247f | ||
|
|
138872e990 | ||
|
|
a0501b4a0a | ||
|
|
7cea13c5c0 | ||
|
|
9a8175903b |
@@ -3510,3 +3510,7 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
|||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'status', 'backlog', 'done', NULL, '2026-06-10 16:57:07', '2026-06-10 16:57:07', '2026-06-10 16:57:07', NULL, '1ce291d54831d3ede687384037c379ff', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'status', 'backlog', 'done', NULL, '2026-06-10 16:57:07', '2026-06-10 16:57:07', '2026-06-10 16:57:07', NULL, '1ce291d54831d3ede687384037c379ff', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'status', 'backlog', 'done', NULL, '2026-06-10 17:13:53', '2026-06-10 17:13:53', '2026-06-10 17:13:53', NULL, '87b069d3f25c91622c9720147d359360', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'status', 'backlog', 'done', NULL, '2026-06-10 17:13:53', '2026-06-10 17:13:53', '2026-06-10 17:13:53', NULL, '87b069d3f25c91622c9720147d359360', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'status', 'backlog', 'done', NULL, '2026-06-10 17:47:01', '2026-06-10 17:47:01', '2026-06-10 17:47:01', NULL, 'ba3d17ec4e668ade82b07d2bb848ab91', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'status', 'backlog', 'done', NULL, '2026-06-10 17:47:01', '2026-06-10 17:47:01', '2026-06-10 17:47:01', NULL, 'ba3d17ec4e668ade82b07d2bb848ab91', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'status', 'backlog', 'done', NULL, '2026-06-10 18:20:28', '2026-06-10 18:20:28', '2026-06-10 18:20:28', NULL, '213109fcd67b4375ebbd3b59c4a1ed04', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'status', 'backlog', 'review', NULL, '2026-06-10 18:24:41', '2026-06-10 18:24:41', '2026-06-10 18:24:41', NULL, 'c5a88f9594c44896a6a3d1a4b2418ed2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'status', 'review', 'done', NULL, '2026-06-10 18:27:58', '2026-06-10 18:27:58', '2026-06-10 18:27:58', NULL, 'a9c72ab53b69f5ca6bf1fa4dd0ddfa05', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'status', 'backlog', 'done', NULL, '2026-06-10 18:38:53', '2026-06-10 18:38:53', '2026-06-10 18:38:53', NULL, 'b94cfe8ba315b3be6775474c681b4e80', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -174,3 +174,6 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
|
|||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'T-347', '2026-06-10 16:55:10', '2026-06-10 16:55:10', NULL, '9d40226dbc6136072d2d5d0eda71f141', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'T-347', '2026-06-10 16:55:10', '2026-06-10 16:55:10', NULL, '9d40226dbc6136072d2d5d0eda71f141', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'T-348', '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, '8bb5ad92551f272417840869a0774668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'T-348', '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, '8bb5ad92551f272417840869a0774668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'T-349', '2026-06-10 17:45:28', '2026-06-10 17:45:28', NULL, '0b12f14fa83254ab113c870531628359', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'T-349', '2026-06-10 17:45:28', '2026-06-10 17:45:28', NULL, '0b12f14fa83254ab113c870531628359', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'T-350', '2026-06-10 18:02:50', '2026-06-10 18:02:50', NULL, 'a5c0c22d84621b14a5208317414d6026', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'T-351', '2026-06-10 18:23:41', '2026-06-10 18:23:41', NULL, '9d2da44c16c5aa38c0a36e4b00ef5f15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'T-352', '2026-06-10 18:34:05', '2026-06-10 18:34:05', NULL, '3fe3e1d5fb7c0fbd084b45116575ad98', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||||
|
|||||||
@@ -3213,3 +3213,30 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
|||||||
So a build host/container missing the Wayland client dev headers (very plausible in a Bazzite/immutable distrobox or toolbox) silently compiles out the decoration suppression — the rebuilt app ships with the compositor''s native title bar (double title bar on KDE Plasma Wayland). Reported live on Bazzite KDE.
|
So a build host/container missing the Wayland client dev headers (very plausible in a Bazzite/immutable distrobox or toolbox) silently compiles out the decoration suppression — the rebuilt app ships with the compositor''s native title bar (double title bar on KDE Plasma Wayland). Reported live on Bazzite KDE.
|
||||||
|
|
||||||
Fix: make wayland-client a hard build requirement — fail the CMake configure with a clear, actionable message (name the package: Fedora wayland-devel, Debian/Ubuntu libwayland-dev) instead of dropping the feature. Frameless chrome is a core guardrail; never ship without it. Also fix the stale ''xdg-decoration'' comment (the code uses the KDE server-decoration protocol, not xdg-decoration).', 'done', 'high', NULL, NULL, NULL, '2026-06-10 17:45:28', '2026-06-10 17:47:01', NULL, '6eb6120c0ef64a7f5ce78073b398143c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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);
|
Fix: make wayland-client a hard build requirement — fail the CMake configure with a clear, actionable message (name the package: Fedora wayland-devel, Debian/Ubuntu libwayland-dev) instead of dropping the feature. Frameless chrome is a core guardrail; never ship without it. Also fix the stale ''xdg-decoration'' comment (the code uses the KDE server-decoration protocol, not xdg-decoration).', 'done', 'high', NULL, NULL, NULL, '2026-06-10 17:45:28', '2026-06-10 17:47:01', NULL, '6eb6120c0ef64a7f5ce78073b398143c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'pql sidebar panes stick on error when the first fetch fires too early', 'The pql-backed sidebar panes (tickets, decisions, pql, search) fetch once on first build via pql.* IPC. If that first fetch hits a TRANSIENT pql failure — the planning DB still settling at startup, or a db-busy SQLite lock under concurrent pql writes (pql 1.10 exits 69 ''db busy'', as seen in the serial test suite) — the pane shows ''pql ticket failed'' and stays there until a manual refresh / tab-switch re-fires the fetch. Reproduced live on 2.3.0: left pane ''pql ticket failed''; works on manual refresh. pql works fine in isolation, so it''s purely a too-early / transient timing issue with no retry.
|
||||||
|
|
||||||
|
Fix: make pql invocations resilient to transient failures at the single chokepoint, PqlClient._run (lib/src/pql/client.dart) — on a busy/locked signal (exit 69, or stderr mentioning database is locked / busy), retry a small bounded number of times with short backoff before throwing PqlException. Keep genuine errors fast (don''t blanket-retry every non-zero). Fixes all pql panes at once.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-10 18:02:50', '2026-06-10 18:02:50', NULL, 'd03c864f5e64284f3582e59556a4ab4f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'pql sidebar panes stick on error when the first fetch fires too early', 'The pql-backed sidebar panes (tickets, decisions, pql, search) fetch once on first build via pql.* IPC. If that first fetch hits a TRANSIENT pql failure — the planning DB still settling at startup, or a db-busy SQLite lock under concurrent pql writes (pql 1.10 exits 69 ''db busy'', as seen in the serial test suite) — the pane shows ''pql ticket failed'' and stays there until a manual refresh / tab-switch re-fires the fetch. Reproduced live on 2.3.0: left pane ''pql ticket failed''; works on manual refresh. pql works fine in isolation, so it''s purely a too-early / transient timing issue with no retry.
|
||||||
|
|
||||||
|
Fix: make pql invocations resilient to transient failures at the single chokepoint, PqlClient._run (lib/src/pql/client.dart) — on a busy/locked signal (exit 69, or stderr mentioning database is locked / busy), retry a small bounded number of times with short backoff before throwing PqlException. Keep genuine errors fast (don''t blanket-retry every non-zero). Fixes all pql panes at once.', 'done', 'high', NULL, NULL, NULL, '2026-06-10 18:02:50', '2026-06-10 18:20:28', NULL, 'd48c86ad83f3da7cd4f8c9308c852448', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'bug', NULL, 'Frameless chrome: KWin keeps the title bar — decoration request fires before the wl_surface exists', 'On KDE Plasma 6 / KWin 6 (Wayland), clide still shows the compositor''s native title bar even though the frameless code (D-057, KDE server-decoration protocol) is compiled in (confirmed: with T-349 making wayland-client REQUIRED, a 2.3.0 build that runs at all has it).
|
||||||
|
|
||||||
|
Root cause is timing, not the protocol. linux/runner/clide_app.cc connects request_no_server_decorations() to the GtkWidget ''realize'' signal, but GTK''s Wayland backend only creates the wl_surface on MAP, not realize. So gdk_wayland_window_get_wl_surface() returns null and the function bails at its own ''if (surface == nullptr) return'' before creating the org_kde_kwin_server_decoration / requesting mode NONE. KWin 6 defaults to server-side decorations on Wayland unless that request lands → native title bar shows (double title bar with clide''s own chrome).
|
||||||
|
|
||||||
|
Fix: also fire the request on the ''map'' signal (wl_surface is live by then); the realize handler still does the X11 gdk_window_set_decorations hint and harmlessly bails on the Wayland part (surface null) so no duplicate decoration object is created. If KWin 6 turns out not to honor the legacy KDE protocol, fall back to the standard xdg-decoration protocol (zxdg_decoration_manager_v1, set_mode CLIENT_SIDE).', 'backlog', 'high', NULL, NULL, NULL, '2026-06-10 18:23:41', '2026-06-10 18:23:41', NULL, 'ec216013c0bb79f1f11a87979c9fb886', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'bug', NULL, 'Frameless chrome: KWin keeps the title bar — decoration request fires before the wl_surface exists', 'On KDE Plasma 6 / KWin 6 (Wayland), clide still shows the compositor''s native title bar even though the frameless code (D-057, KDE server-decoration protocol) is compiled in (confirmed: with T-349 making wayland-client REQUIRED, a 2.3.0 build that runs at all has it).
|
||||||
|
|
||||||
|
Root cause is timing, not the protocol. linux/runner/clide_app.cc connects request_no_server_decorations() to the GtkWidget ''realize'' signal, but GTK''s Wayland backend only creates the wl_surface on MAP, not realize. So gdk_wayland_window_get_wl_surface() returns null and the function bails at its own ''if (surface == nullptr) return'' before creating the org_kde_kwin_server_decoration / requesting mode NONE. KWin 6 defaults to server-side decorations on Wayland unless that request lands → native title bar shows (double title bar with clide''s own chrome).
|
||||||
|
|
||||||
|
Fix: also fire the request on the ''map'' signal (wl_surface is live by then); the realize handler still does the X11 gdk_window_set_decorations hint and harmlessly bails on the Wayland part (surface null) so no duplicate decoration object is created. If KWin 6 turns out not to honor the legacy KDE protocol, fall back to the standard xdg-decoration protocol (zxdg_decoration_manager_v1, set_mode CLIENT_SIDE).', 'review', 'high', NULL, NULL, NULL, '2026-06-10 18:23:41', '2026-06-10 18:24:41', NULL, '0d06f87ff4d71e2ee4e4821968b5e3df', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'bug', NULL, 'Frameless chrome: KWin keeps the title bar — decoration request fires before the wl_surface exists', 'On KDE Plasma 6 / KWin 6 (Wayland), clide still shows the compositor''s native title bar even though the frameless code (D-057, KDE server-decoration protocol) is compiled in (confirmed: with T-349 making wayland-client REQUIRED, a 2.3.0 build that runs at all has it).
|
||||||
|
|
||||||
|
Root cause is timing, not the protocol. linux/runner/clide_app.cc connects request_no_server_decorations() to the GtkWidget ''realize'' signal, but GTK''s Wayland backend only creates the wl_surface on MAP, not realize. So gdk_wayland_window_get_wl_surface() returns null and the function bails at its own ''if (surface == nullptr) return'' before creating the org_kde_kwin_server_decoration / requesting mode NONE. KWin 6 defaults to server-side decorations on Wayland unless that request lands → native title bar shows (double title bar with clide''s own chrome).
|
||||||
|
|
||||||
|
Fix: also fire the request on the ''map'' signal (wl_surface is live by then); the realize handler still does the X11 gdk_window_set_decorations hint and harmlessly bails on the Wayland part (surface null) so no duplicate decoration object is created. If KWin 6 turns out not to honor the legacy KDE protocol, fall back to the standard xdg-decoration protocol (zxdg_decoration_manager_v1, set_mode CLIENT_SIDE).', 'done', 'high', NULL, NULL, NULL, '2026-06-10 18:23:41', '2026-06-10 18:27:58', NULL, 'e61eeca3f6ccf42a334867c757fda759', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket sidebar errors on first load: pql runs before the workspace workDir is set', 'The tickets sidebar (and other pql panes) fail on first load when clide is desktop-launched: the daemon''s PqlClient is constructed with workDir = the boot CWD (the launch dir, e.g. ~), not the repo. The pane''s first pql.tickets.list fires before swapIpcServer reconfigures the dispatcher with the project''s workRoot, so pql runs in the wrong dir — against a stale/global pql.db — and errors (observed: ''ticket_deps.blocker_record_id missing — pql.db is from an earlier schema''). A manual refresh works because by then the workspace is open and the workDir is correct.
|
||||||
|
|
||||||
|
This is a wrong-workDir timing issue, not db-busy (so the T-350 retry doesn''t catch it). Fix: the pql-backed panes refetch on ProjectOpened (which fires after the IPC server swaps to the project workRoot). Implemented for the tickets pane; the decisions/pql/search panes share the latent bug and should get the same refetch.', 'backlog', 'high', NULL, NULL, NULL, '2026-06-10 18:34:05', '2026-06-10 18:34:05', NULL, 'a6d1821c06dbccb30c9f7e777aef7ef5', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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 (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket sidebar errors on first load: pql runs before the workspace workDir is set', 'The tickets sidebar (and other pql panes) fail on first load when clide is desktop-launched: the daemon''s PqlClient is constructed with workDir = the boot CWD (the launch dir, e.g. ~), not the repo. The pane''s first pql.tickets.list fires before swapIpcServer reconfigures the dispatcher with the project''s workRoot, so pql runs in the wrong dir — against a stale/global pql.db — and errors (observed: ''ticket_deps.blocker_record_id missing — pql.db is from an earlier schema''). A manual refresh works because by then the workspace is open and the workDir is correct.
|
||||||
|
|
||||||
|
This is a wrong-workDir timing issue, not db-busy (so the T-350 retry doesn''t catch it). Fix: the pql-backed panes refetch on ProjectOpened (which fires after the IPC server swaps to the project workRoot). Implemented for the tickets pane; the decisions/pql/search panes share the latent bug and should get the same refetch.', 'done', 'high', NULL, NULL, NULL, '2026-06-10 18:34:05', '2026-06-10 18:38:53', NULL, '3891f832c1d930d8fefca2fae7ffea86', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_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);
|
||||||
|
|||||||
@@ -16,6 +16,39 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.3.2] — 2026-06-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Ticket and decision sidebars reliably load on first open (real fix).** The
|
||||||
|
2.3.1 re-fetch-on-open helped only when a project is picked *after* the window
|
||||||
|
is up; with sticky-startup the project opens during boot, before the panes
|
||||||
|
mount, so they never saw the event. The underlying cause was a race: the boot
|
||||||
|
IPC-server swap (to the launch CWD) and the project-open swap (to the repo)
|
||||||
|
ran concurrently, and the late-finishing boot swap could clobber the repo
|
||||||
|
bind — leaving the daemon's pql/git/files pointed at the launch directory
|
||||||
|
(HOME) and the sidebars erroring on a stale/global pql.db. Swaps are now
|
||||||
|
serialized so the repo bind always wins. (T-352)
|
||||||
|
|
||||||
|
## [2.3.1] — 2026-06-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Frameless window chrome works on KDE Plasma 6 / KWin 6.** The Wayland
|
||||||
|
server-decoration request fired on `realize`, before GTK created the
|
||||||
|
surface, so it bailed and KWin (which defaults to server-side decorations)
|
||||||
|
kept drawing its own title bar. It now also fires on `map`. (T-351)
|
||||||
|
- **pql sidebar panes no longer stick on a transient startup error.** A
|
||||||
|
too-early or db-busy pql failure (the planning DB still settling, or a
|
||||||
|
SQLite lock under concurrent writes) is now retried a few times before
|
||||||
|
surfacing, instead of leaving the pane on "pql … failed" until a manual
|
||||||
|
refresh. (T-350)
|
||||||
|
- **Ticket and decision sidebars load on first open, not just after a manual
|
||||||
|
refresh.** On a desktop launch the daemon's pql workspace starts as the
|
||||||
|
launch directory, not the repo, so the panes' first fetch ran against the
|
||||||
|
wrong (or a stale-schema) DB and errored. They now re-fetch when the
|
||||||
|
workspace actually opens, by which point the pql workspace is the repo. (T-352)
|
||||||
|
|
||||||
## [2.3.0] — 2026-06-10
|
## [2.3.0] — 2026-06-10
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ self:
|
|||||||
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
|
# Auto-synced from pubspec.yaml `version:` by `make gen-build-info`
|
||||||
# (runs implicitly on every build/run/test). Don't hand-edit; bump
|
# (runs implicitly on every build/run/test). Don't hand-edit; bump
|
||||||
# pubspec instead.
|
# pubspec instead.
|
||||||
version: "2.3.0"
|
version: "2.3.2"
|
||||||
homepage: https://github.com/postmeridiem/clide
|
homepage: https://github.com/postmeridiem/clide
|
||||||
license: MIT
|
license: MIT
|
||||||
license_file: assets/LICENSE
|
license_file: assets/LICENSE
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
|||||||
StreamSubscription<Message>? _focusSub;
|
StreamSubscription<Message>? _focusSub;
|
||||||
StreamSubscription<DaemonEvent>? _fileSub;
|
StreamSubscription<DaemonEvent>? _fileSub;
|
||||||
StreamSubscription<SchedulerTick>? _schedulerSub;
|
StreamSubscription<SchedulerTick>? _schedulerSub;
|
||||||
|
StreamSubscription<ProjectOpened>? _projectSub;
|
||||||
bool _refreshing = false;
|
bool _refreshing = false;
|
||||||
bool _pendingRefresh = false;
|
bool _pendingRefresh = false;
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ class _DecisionsViewState extends State<DecisionsView> {
|
|||||||
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? ''))
|
.where((e) => e.subsystem == 'files' && e.kind == 'files.changed' && _isDecisionPath(e.data['path'] as String? ?? ''))
|
||||||
.listen((_) => _refresh());
|
.listen((_) => _refresh());
|
||||||
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
|
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
|
||||||
|
// The first load can fire before the project's workspace is wired into
|
||||||
|
// the daemon (the boot workDir is the launch CWD, not the repo), so pql
|
||||||
|
// runs against the wrong/old DB and the list errors. Re-fetch once the
|
||||||
|
// workspace is actually open — ProjectOpened fires after the IPC server
|
||||||
|
// swaps to the project workRoot. (T-352)
|
||||||
|
_projectSub = kernel.events.on<ProjectOpened>().listen((_) => _refresh());
|
||||||
}
|
}
|
||||||
if (!_loading || _decisions.isNotEmpty) return;
|
if (!_loading || _decisions.isNotEmpty) return;
|
||||||
unawaited(_load());
|
unawaited(_load());
|
||||||
@@ -88,6 +95,7 @@ class _DecisionsViewState extends State<DecisionsView> {
|
|||||||
_focusSub?.cancel();
|
_focusSub?.cancel();
|
||||||
_fileSub?.cancel();
|
_fileSub?.cancel();
|
||||||
_schedulerSub?.cancel();
|
_schedulerSub?.cancel();
|
||||||
|
_projectSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class _TicketsViewState extends State<TicketsView> {
|
|||||||
StreamSubscription<Message>? _focusSub;
|
StreamSubscription<Message>? _focusSub;
|
||||||
StreamSubscription<SchedulerTick>? _schedulerSub;
|
StreamSubscription<SchedulerTick>? _schedulerSub;
|
||||||
StreamSubscription<Message>? _changedSub;
|
StreamSubscription<Message>? _changedSub;
|
||||||
|
StreamSubscription<ProjectOpened>? _projectSub;
|
||||||
bool _refreshing = false;
|
bool _refreshing = false;
|
||||||
bool _pendingRefresh = false;
|
bool _pendingRefresh = false;
|
||||||
|
|
||||||
@@ -111,6 +112,12 @@ class _TicketsViewState extends State<TicketsView> {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
|
_schedulerSub = kernel.events.on<SchedulerTick>().where((e) => e.tier == SchedulerTier.oneMinute).listen((_) => _refresh());
|
||||||
|
// The first load can fire before the project's workspace is wired into
|
||||||
|
// the daemon (the boot workDir is the launch CWD, not the repo), so pql
|
||||||
|
// runs against the wrong/old DB and the list errors. Re-fetch once the
|
||||||
|
// workspace is actually open — the daemon's pql workDir is correct by
|
||||||
|
// then (ProjectOpened fires after the IPC server swaps). (T-352)
|
||||||
|
_projectSub = kernel.events.on<ProjectOpened>().listen((_) => _refresh());
|
||||||
}
|
}
|
||||||
if (!_loading || _tickets.isNotEmpty) return;
|
if (!_loading || _tickets.isNotEmpty) return;
|
||||||
unawaited(_load());
|
unawaited(_load());
|
||||||
@@ -121,6 +128,7 @@ class _TicketsViewState extends State<TicketsView> {
|
|||||||
_focusSub?.cancel();
|
_focusSub?.cancel();
|
||||||
_changedSub?.cancel();
|
_changedSub?.cancel();
|
||||||
_schedulerSub?.cancel();
|
_schedulerSub?.cancel();
|
||||||
|
_projectSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -116,7 +116,11 @@ Future<void> main() async {
|
|||||||
McpServer? mcpServer;
|
McpServer? mcpServer;
|
||||||
final ipcLog = Logger();
|
final ipcLog = Logger();
|
||||||
|
|
||||||
Future<void> swapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) async {
|
// IPC-server swaps must run one-at-a-time — see the swapIpcServer wrapper
|
||||||
|
// below doSwapIpcServer for why. (T-352)
|
||||||
|
Future<void> swapChain = Future<void>.value();
|
||||||
|
|
||||||
|
Future<void> doSwapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) async {
|
||||||
if (kIsWeb) return;
|
if (kIsWeb) return;
|
||||||
// Already serving this exact workspace? Reuse the live server.
|
// Already serving this exact workspace? Reuse the live server.
|
||||||
// The startup factory binds the launch CWD, then the project-open
|
// The startup factory binds the launch CWD, then the project-open
|
||||||
@@ -173,6 +177,22 @@ Future<void> main() async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize IPC-server swaps. The boot factory fires a swap to the launch
|
||||||
|
// CWD with unawaited(); the project-open flow then fires another to the
|
||||||
|
// real repo. Unserialized, the two interleave and the late-finishing boot
|
||||||
|
// swap can clobber the repo bind — reconnecting the daemon client to the
|
||||||
|
// launch-CWD (HOME) socket, so pql/git/files run against the wrong
|
||||||
|
// workspace. That surfaced as the ticket/decision sidebars failing on first
|
||||||
|
// load (stale/global pql.db) yet working after a manual refresh. Chaining
|
||||||
|
// every swap makes them apply in call order; the repo swap is issued last
|
||||||
|
// and therefore wins. (T-352)
|
||||||
|
Future<void> swapIpcServer(DaemonDispatcher dispatcher, Directory workRoot) {
|
||||||
|
final next = swapChain.then((_) => doSwapIpcServer(dispatcher, workRoot));
|
||||||
|
// A failed swap must not break the chain for the next one.
|
||||||
|
swapChain = next.catchError((Object _) {});
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
DaemonDispatcher buildDispatcher(
|
DaemonDispatcher buildDispatcher(
|
||||||
DaemonBus events,
|
DaemonBus events,
|
||||||
Toolchain tc,
|
Toolchain tc,
|
||||||
|
|||||||
+52
-26
@@ -162,33 +162,59 @@ class PqlClient {
|
|||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// pql's exit code for a locked / unavailable planning DB (EX_UNAVAILABLE) —
|
||||||
|
/// a transient SQLite-busy condition under concurrent access (T-350).
|
||||||
|
static const int _kBusyExitCode = 69;
|
||||||
|
static const int _kMaxAttempts = 4;
|
||||||
|
|
||||||
Future<Object?> _run(List<String> args) async {
|
Future<Object?> _run(List<String> args) async {
|
||||||
final ProcessResult r;
|
for (var attempt = 1; attempt <= _kMaxAttempts; attempt++) {
|
||||||
try {
|
final ProcessResult r;
|
||||||
r = await Process.run(
|
try {
|
||||||
toolchain.pql,
|
r = await Process.run(
|
||||||
args,
|
toolchain.pql,
|
||||||
workingDirectory: workDir.path,
|
args,
|
||||||
);
|
workingDirectory: workDir.path,
|
||||||
} on ProcessException catch (e) {
|
);
|
||||||
throw PqlException(
|
} on ProcessException catch (e) {
|
||||||
'pql ${args.first}: ${e.message}',
|
throw PqlException(
|
||||||
exitCode: e.errorCode,
|
'pql ${args.first}: ${e.message}',
|
||||||
stderr: e.toString(),
|
exitCode: e.errorCode,
|
||||||
);
|
stderr: e.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final stderr = (r.stderr as String).trim();
|
||||||
|
// pql 1.5+ returns exit 0 with an empty `[]` for zero matches, so any
|
||||||
|
// non-zero exit is a real error (older pql used exit 2 for empty).
|
||||||
|
if (r.exitCode != 0) {
|
||||||
|
// A transient db-busy / still-settling failure — a sidebar pane firing
|
||||||
|
// its one-shot fetch too early at startup, or contention from
|
||||||
|
// concurrent pql writes — would otherwise stick until a manual refresh.
|
||||||
|
// Retry a few times with short backoff first. Genuine errors aren't
|
||||||
|
// busy, so they still surface immediately. (T-350)
|
||||||
|
if (attempt < _kMaxAttempts && _isTransient(r.exitCode, stderr)) {
|
||||||
|
await Future<void>.delayed(Duration(milliseconds: 100 * attempt));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw PqlException(
|
||||||
|
'pql ${args.first} failed',
|
||||||
|
exitCode: r.exitCode,
|
||||||
|
stderr: stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final stdout = (r.stdout as String).trim();
|
||||||
|
if (stdout.isEmpty) return null;
|
||||||
|
return jsonDecode(stdout);
|
||||||
}
|
}
|
||||||
final stderr = (r.stderr as String).trim();
|
// Unreachable: the loop returns, continues, or throws on the final attempt.
|
||||||
// pql 1.5+ returns exit 0 with an empty `[]` for zero matches, so any
|
throw StateError('pql retry loop exhausted without a result');
|
||||||
// non-zero exit is a real error (older pql used exit 2 for empty).
|
}
|
||||||
if (r.exitCode != 0) {
|
|
||||||
throw PqlException(
|
/// Whether a non-zero pql exit looks like a transient db-busy / not-yet-ready
|
||||||
'pql ${args.first} failed',
|
/// condition worth retrying, vs. a genuine error to surface immediately.
|
||||||
exitCode: r.exitCode,
|
static bool _isTransient(int exitCode, String stderr) {
|
||||||
stderr: stderr,
|
if (exitCode == _kBusyExitCode) return true;
|
||||||
);
|
final s = stderr.toLowerCase();
|
||||||
}
|
return s.contains('database is locked') || s.contains('db busy') || s.contains('database busy') || s.contains('locked');
|
||||||
final stdout = (r.stdout as String).trim();
|
|
||||||
if (stdout.isEmpty) return null;
|
|
||||||
return jsonDecode(stdout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,13 @@ static void clide_app_activate(GApplication* application) {
|
|||||||
// D-057: frameless custom chrome.
|
// D-057: frameless custom chrome.
|
||||||
gtk_window_set_decorated(window, FALSE);
|
gtk_window_set_decorated(window, FALSE);
|
||||||
gtk_window_set_title(window, "clide");
|
gtk_window_set_title(window, "clide");
|
||||||
|
// Run the decoration suppression on both realize (the X11 hint) and map. The
|
||||||
|
// Wayland server-decoration request needs a live wl_surface, which GTK only
|
||||||
|
// creates on map — at realize gdk_wayland_window_get_wl_surface() is still
|
||||||
|
// null and the request bails, leaving KWin (which defaults to server-side
|
||||||
|
// decorations on Wayland) to draw its own title bar (T-351).
|
||||||
g_signal_connect(window, "realize", G_CALLBACK(on_window_realize), nullptr);
|
g_signal_connect(window, "realize", G_CALLBACK(on_window_realize), nullptr);
|
||||||
|
g_signal_connect(window, "map", G_CALLBACK(on_window_realize), nullptr);
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ description: >-
|
|||||||
subsystem handlers (pane, files, editor, git, pql), and the
|
subsystem handlers (pane, files, editor, git, pql), and the
|
||||||
extension framework.
|
extension framework.
|
||||||
publish_to: none
|
publish_to: none
|
||||||
version: 2.3.0
|
version: 2.3.2
|
||||||
repository: https://github.com/postmeridiem/clide
|
repository: https://github.com/postmeridiem/clide
|
||||||
# Short user-facing tagline (the welcome subtitle, web meta
|
# Short user-facing tagline (the welcome subtitle, web meta
|
||||||
# description, etc.). Baked into lib/src/build_info.g.dart by
|
# description, etc.). Baked into lib/src/build_info.g.dart by
|
||||||
|
|||||||
@@ -546,6 +546,29 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('DecisionsView — workspace open triggers reload (T-352)', () {
|
||||||
|
testWidgets('ProjectOpened triggers _refresh', (tester) async {
|
||||||
|
// The first load can fire before the daemon's pql workDir is the repo; a
|
||||||
|
// ProjectOpened (fired after the IPC server swaps) must re-fetch.
|
||||||
|
int listCallCount = 0;
|
||||||
|
f.ipc.stub('pql.decisions.sync', (_) async => _ok(const {}));
|
||||||
|
f.ipc.stub('pql.decisions.list', (_) async {
|
||||||
|
listCallCount++;
|
||||||
|
return _ok({
|
||||||
|
'decisions': [_decision(id: 'D-$listCallCount', title: 'open $listCallCount')],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await pumpView(tester);
|
||||||
|
expect(listCallCount, 1);
|
||||||
|
|
||||||
|
f.services.events.emit(const ProjectOpened(path: '/repo'));
|
||||||
|
await pumpAsync(tester);
|
||||||
|
|
||||||
|
expect(listCallCount, greaterThanOrEqualTo(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('DecisionsView — concurrent refresh guard', () {
|
group('DecisionsView — concurrent refresh guard', () {
|
||||||
testWidgets('second refresh while one is running sets _pendingRefresh', (tester) async {
|
testWidgets('second refresh while one is running sets _pendingRefresh', (tester) async {
|
||||||
final Completer<IpcResponse> firstListCompleter = Completer();
|
final Completer<IpcResponse> firstListCompleter = Completer();
|
||||||
|
|||||||
@@ -137,6 +137,21 @@ void main() {
|
|||||||
expect(calls, greaterThan(before));
|
expect(calls, greaterThan(before));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('refetches when the workspace opens (T-352)', (tester) async {
|
||||||
|
// The first load can fire before the daemon's pql workDir is the repo; a
|
||||||
|
// ProjectOpened (fired after the IPC server swaps) must re-fetch.
|
||||||
|
var calls = 0;
|
||||||
|
f.ipc.stub('pql.tickets.list', (_) async {
|
||||||
|
calls++;
|
||||||
|
return _list([_t('T-1', 'Thing', 'backlog')]);
|
||||||
|
});
|
||||||
|
await pumpView(tester);
|
||||||
|
final before = calls;
|
||||||
|
f.services.events.emit(const ProjectOpened(path: '/repo'));
|
||||||
|
await pumpAsync(tester);
|
||||||
|
expect(calls, greaterThan(before));
|
||||||
|
});
|
||||||
|
|
||||||
// -- Type-filter chips (T-343) ------------------------------------------
|
// -- Type-filter chips (T-343) ------------------------------------------
|
||||||
// The chips own one GestureDetector for both onTap (toggle) and onDoubleTap
|
// The chips own one GestureDetector for both onTap (toggle) and onDoubleTap
|
||||||
// (solo), so a single tap's onTap only fires after the ~300ms double-tap
|
// (solo), so a single tap's onTap only fires after the ~300ms double-tap
|
||||||
|
|||||||
@@ -150,4 +150,43 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('PqlClient — transient retry (T-350)', () {
|
||||||
|
late Directory tmp;
|
||||||
|
setUp(() async => tmp = await Directory.systemTemp.createTemp('clide_fakepql_'));
|
||||||
|
tearDown(() async {
|
||||||
|
if (await tmp.exists()) await tmp.delete(recursive: true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A fake `pql` whose body is [body]; a fresh `$0.n` counter file per test
|
||||||
|
// lets a script "recover" after N invocations.
|
||||||
|
Future<PqlClient> fakePql(String body) async {
|
||||||
|
final f = File('${tmp.path}/pql');
|
||||||
|
await f.writeAsString('#!/bin/sh\n$body\n');
|
||||||
|
await Process.run('chmod', ['+x', f.path]);
|
||||||
|
return PqlClient(workDir: Directory.current, toolchain: ToolchainView.resolved(ResolvedPaths(pql: f.path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a genuine (non-busy) error surfaces immediately', () async {
|
||||||
|
final p = await fakePql('exit 2');
|
||||||
|
await expectLater(p.files(), throwsA(isA<PqlException>().having((e) => e.exitCode, 'exitCode', 2)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a persistent db-busy (exit 69) throws after exhausting retries', () async {
|
||||||
|
final p = await fakePql('exit 69');
|
||||||
|
await expectLater(p.files(), throwsA(isA<PqlException>().having((e) => e.exitCode, 'exitCode', 69)));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a transient db-busy (exit 69) recovers on retry', () async {
|
||||||
|
final p = await fakePql(r'c="$0.n"; n=$(cat "$c" 2>/dev/null || echo 0); n=$((n+1)); echo "$n" > "$c"; '
|
||||||
|
r'if [ "$n" -lt 3 ]; then exit 69; fi; echo "[]"');
|
||||||
|
expect(await p.files(), isEmpty); // retried through two busies to success
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a "database is locked" stderr (non-69 exit) is also retried', () async {
|
||||||
|
final p = await fakePql(r'c="$0.n"; n=$(cat "$c" 2>/dev/null || echo 0); n=$((n+1)); echo "$n" > "$c"; '
|
||||||
|
r'if [ "$n" -lt 3 ]; then echo "database is locked" >&2; exit 1; fi; echo "[]"');
|
||||||
|
expect(await p.files(), isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user