feat(tui): add snapshot test infrastructure + terminal compat matrix (bd-2nfs)
- 6 deterministic snapshot tests at 120x40 with FakeClock frozen at 2026-01-15T12:00:00Z - Buffer-to-plaintext serializer resolving chars, graphemes, and wide-char continuations - Golden file management with UPDATE_SNAPSHOTS=1 env var for regeneration - Snapshot diff output on mismatch for easy debugging - Tests: dashboard, issue list, issue detail, MR list, search results, empty state - TERMINAL_COMPAT.md template for manual QA across iTerm2/tmux/Alacritty/kitty/WezTerm
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
bd-3rjw
|
||||
bd-2nfs
|
||||
|
||||
61
crates/lore-tui/TERMINAL_COMPAT.md
Normal file
61
crates/lore-tui/TERMINAL_COMPAT.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Terminal Compatibility Matrix
|
||||
|
||||
Manual verification checklist for lore-tui rendering across terminal emulators.
|
||||
|
||||
**How to use:** Run `cargo run -p lore-tui` in each terminal, navigate through
|
||||
all screens, and mark each cell with one of:
|
||||
- OK — works correctly
|
||||
- PARTIAL — works with minor visual glitches (describe in Notes)
|
||||
- FAIL — broken or unusable (describe in Notes)
|
||||
- N/T — not tested
|
||||
|
||||
Last verified: _not yet_
|
||||
|
||||
## Rendering Features
|
||||
|
||||
| Feature | iTerm2 | tmux | Alacritty | kitty | WezTerm |
|
||||
|----------------------|--------|------|-----------|-------|---------|
|
||||
| True color (RGB) | | | | | |
|
||||
| Unicode box-drawing | | | | | |
|
||||
| CJK wide characters | | | | | |
|
||||
| Bold text | | | | | |
|
||||
| Italic text | | | | | |
|
||||
| Underline | | | | | |
|
||||
| Dim / faint | | | | | |
|
||||
| Strikethrough | | | | | |
|
||||
|
||||
## Interaction Features
|
||||
|
||||
| Feature | iTerm2 | tmux | Alacritty | kitty | WezTerm |
|
||||
|----------------------|--------|------|-----------|-------|---------|
|
||||
| Keyboard input | | | | | |
|
||||
| Mouse click | | | | | |
|
||||
| Mouse scroll | | | | | |
|
||||
| Resize handling | | | | | |
|
||||
| Alt screen toggle | | | | | |
|
||||
| Bracketed paste | | | | | |
|
||||
|
||||
## Screen-Specific Checks
|
||||
|
||||
| Screen | iTerm2 | tmux | Alacritty | kitty | WezTerm |
|
||||
|----------------------|--------|------|-----------|-------|---------|
|
||||
| Dashboard | | | | | |
|
||||
| Issue list | | | | | |
|
||||
| Issue detail | | | | | |
|
||||
| MR list | | | | | |
|
||||
| MR detail | | | | | |
|
||||
| Search | | | | | |
|
||||
| Command palette | | | | | |
|
||||
| Help overlay | | | | | |
|
||||
|
||||
## Minimum Sizes
|
||||
|
||||
| Terminal size | Renders correctly? | Notes |
|
||||
|---------------|-------------------|-------|
|
||||
| 80x24 | | |
|
||||
| 120x40 | | |
|
||||
| 200x60 | | |
|
||||
|
||||
## Notes
|
||||
|
||||
_Record any issues, workarounds, or version-specific quirks here._
|
||||
453
crates/lore-tui/tests/snapshot_tests.rs
Normal file
453
crates/lore-tui/tests/snapshot_tests.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
//! Snapshot tests for deterministic TUI rendering.
|
||||
//!
|
||||
//! Each test renders a screen at a fixed terminal size (120x40) with
|
||||
//! FakeClock frozen at 2026-01-15T12:00:00Z, then compares the plain-text
|
||||
//! output against a golden file in `tests/snapshots/`.
|
||||
//!
|
||||
//! To update golden files after intentional changes:
|
||||
//! UPDATE_SNAPSHOTS=1 cargo test -p lore-tui snapshot
|
||||
//!
|
||||
//! Golden files are UTF-8 plain text with LF line endings, diffable in VCS.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use ftui::Model;
|
||||
use ftui::render::frame::Frame;
|
||||
use ftui::render::grapheme_pool::GraphemePool;
|
||||
|
||||
use lore_tui::app::LoreApp;
|
||||
use lore_tui::clock::FakeClock;
|
||||
use lore_tui::message::{EntityKey, Msg, Screen, SearchResult};
|
||||
use lore_tui::state::dashboard::{DashboardData, EntityCounts, LastSyncInfo, ProjectSyncInfo};
|
||||
use lore_tui::state::issue_detail::{IssueDetailData, IssueMetadata};
|
||||
use lore_tui::state::issue_list::{IssueListPage, IssueListRow};
|
||||
use lore_tui::state::mr_list::{MrListPage, MrListRow};
|
||||
use lore_tui::task_supervisor::TaskKey;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Fixed terminal size for all snapshot tests.
|
||||
const WIDTH: u16 = 120;
|
||||
const HEIGHT: u16 = 40;
|
||||
|
||||
/// Frozen clock epoch: 2026-01-15T12:00:00Z.
|
||||
fn frozen_clock() -> FakeClock {
|
||||
FakeClock::new(Utc.with_ymd_and_hms(2026, 1, 15, 12, 0, 0).unwrap())
|
||||
}
|
||||
|
||||
/// Path to the snapshots directory (relative to crate root).
|
||||
fn snapshots_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Buffer serializer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Serialize a Frame's buffer to plain text.
|
||||
///
|
||||
/// - Direct chars are rendered as-is.
|
||||
/// - Grapheme references are resolved via the pool.
|
||||
/// - Continuation cells (wide char trailing cells) are skipped.
|
||||
/// - Empty cells become spaces.
|
||||
/// - Each row is right-trimmed and joined with '\n'.
|
||||
fn serialize_frame(frame: &Frame<'_>) -> String {
|
||||
let w = frame.buffer.width();
|
||||
let h = frame.buffer.height();
|
||||
let mut lines = Vec::with_capacity(h as usize);
|
||||
|
||||
for y in 0..h {
|
||||
let mut row = String::with_capacity(w as usize);
|
||||
for x in 0..w {
|
||||
if let Some(cell) = frame.buffer.get(x, y) {
|
||||
let content = cell.content;
|
||||
if content.is_continuation() {
|
||||
// Skip — part of a wide character already rendered.
|
||||
continue;
|
||||
} else if content.is_empty() {
|
||||
row.push(' ');
|
||||
} else if let Some(ch) = content.as_char() {
|
||||
row.push(ch);
|
||||
} else if let Some(gid) = content.grapheme_id() {
|
||||
if let Some(grapheme) = frame.pool.get(gid) {
|
||||
row.push_str(grapheme);
|
||||
} else {
|
||||
row.push('?'); // Fallback for unresolved grapheme.
|
||||
}
|
||||
} else {
|
||||
row.push(' ');
|
||||
}
|
||||
} else {
|
||||
row.push(' ');
|
||||
}
|
||||
}
|
||||
lines.push(row.trim_end().to_string());
|
||||
}
|
||||
|
||||
// Trim trailing empty lines.
|
||||
while lines.last().is_some_and(|l| l.is_empty()) {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
let mut result = lines.join("\n");
|
||||
result.push('\n'); // Trailing newline for VCS friendliness.
|
||||
result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Snapshot assertion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compare rendered output against a golden file.
|
||||
///
|
||||
/// If `UPDATE_SNAPSHOTS=1` is set, overwrites the golden file instead.
|
||||
/// On mismatch, prints a clear diff showing expected vs actual.
|
||||
fn assert_snapshot(name: &str, actual: &str) {
|
||||
let path = snapshots_dir().join(format!("{name}.snap"));
|
||||
|
||||
if std::env::var("UPDATE_SNAPSHOTS").is_ok() {
|
||||
std::fs::write(&path, actual).unwrap_or_else(|e| {
|
||||
panic!("Failed to write snapshot {}: {e}", path.display());
|
||||
});
|
||||
eprintln!("Updated snapshot: {}", path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
if !path.exists() {
|
||||
panic!(
|
||||
"Golden file missing: {}\n\
|
||||
Run with UPDATE_SNAPSHOTS=1 to create it.\n\
|
||||
Actual output:\n{}",
|
||||
path.display(),
|
||||
actual
|
||||
);
|
||||
}
|
||||
|
||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||
panic!("Failed to read snapshot {}: {e}", path.display());
|
||||
});
|
||||
|
||||
if actual != expected {
|
||||
// Print a useful diff.
|
||||
let actual_lines: Vec<&str> = actual.lines().collect();
|
||||
let expected_lines: Vec<&str> = expected.lines().collect();
|
||||
let max = actual_lines.len().max(expected_lines.len());
|
||||
|
||||
let mut diff = String::new();
|
||||
for i in 0..max {
|
||||
let a = actual_lines.get(i).copied().unwrap_or("");
|
||||
let e = expected_lines.get(i).copied().unwrap_or("");
|
||||
if a != e {
|
||||
diff.push_str(&format!(" line {i:3}: expected: {e:?}\n"));
|
||||
diff.push_str(&format!(" line {i:3}: actual: {a:?}\n"));
|
||||
}
|
||||
}
|
||||
|
||||
panic!(
|
||||
"Snapshot mismatch: {}\n\
|
||||
Run with UPDATE_SNAPSHOTS=1 to update.\n\n\
|
||||
Differences:\n{diff}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn test_app() -> LoreApp {
|
||||
let mut app = LoreApp::new();
|
||||
app.clock = Box::new(frozen_clock());
|
||||
app
|
||||
}
|
||||
|
||||
fn render_app(app: &LoreApp) -> String {
|
||||
let mut pool = GraphemePool::new();
|
||||
let mut frame = Frame::new(WIDTH, HEIGHT, &mut pool);
|
||||
app.view(&mut frame);
|
||||
serialize_frame(&frame)
|
||||
}
|
||||
|
||||
// -- Synthetic data fixtures ------------------------------------------------
|
||||
|
||||
fn fixture_dashboard_data() -> DashboardData {
|
||||
DashboardData {
|
||||
counts: EntityCounts {
|
||||
issues_total: 42,
|
||||
issues_open: 15,
|
||||
mrs_total: 28,
|
||||
mrs_open: 7,
|
||||
discussions: 120,
|
||||
notes_total: 350,
|
||||
notes_system_pct: 18,
|
||||
documents: 85,
|
||||
embeddings: 200,
|
||||
},
|
||||
projects: vec![
|
||||
ProjectSyncInfo {
|
||||
path: "infra/platform".into(),
|
||||
minutes_since_sync: 5,
|
||||
},
|
||||
ProjectSyncInfo {
|
||||
path: "web/frontend".into(),
|
||||
minutes_since_sync: 12,
|
||||
},
|
||||
ProjectSyncInfo {
|
||||
path: "api/backend".into(),
|
||||
minutes_since_sync: 8,
|
||||
},
|
||||
ProjectSyncInfo {
|
||||
path: "tools/scripts".into(),
|
||||
minutes_since_sync: 4,
|
||||
},
|
||||
],
|
||||
recent: vec![],
|
||||
last_sync: Some(LastSyncInfo {
|
||||
status: "succeeded".into(),
|
||||
// 2026-01-15T11:55:00Z — 5 min before frozen clock.
|
||||
finished_at: Some(1_736_942_100_000),
|
||||
command: "sync".into(),
|
||||
error: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_issue_list() -> IssueListPage {
|
||||
IssueListPage {
|
||||
rows: vec![
|
||||
IssueListRow {
|
||||
project_path: "infra/platform".into(),
|
||||
iid: 101,
|
||||
title: "Add retry logic for transient failures".into(),
|
||||
state: "opened".into(),
|
||||
author: "alice".into(),
|
||||
labels: vec!["backend".into(), "reliability".into()],
|
||||
updated_at: 1_736_942_000_000, // ~5 min before frozen
|
||||
},
|
||||
IssueListRow {
|
||||
project_path: "web/frontend".into(),
|
||||
iid: 55,
|
||||
title: "Dark mode toggle not persisting across sessions".into(),
|
||||
state: "opened".into(),
|
||||
author: "bob".into(),
|
||||
labels: vec!["ui".into(), "bug".into()],
|
||||
updated_at: 1_736_938_400_000, // ~1 hr before frozen
|
||||
},
|
||||
IssueListRow {
|
||||
project_path: "api/backend".into(),
|
||||
iid: 203,
|
||||
title: "Migrate user service to async runtime".into(),
|
||||
state: "closed".into(),
|
||||
author: "carol".into(),
|
||||
labels: vec!["backend".into(), "refactor".into()],
|
||||
updated_at: 1_736_856_000_000, // ~1 day before frozen
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
total_count: 3,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_issue_detail() -> IssueDetailData {
|
||||
IssueDetailData {
|
||||
metadata: IssueMetadata {
|
||||
iid: 101,
|
||||
project_path: "infra/platform".into(),
|
||||
title: "Add retry logic for transient failures".into(),
|
||||
description: "## Problem\n\nTransient network failures cause cascading \
|
||||
errors in the ingestion pipeline. We need exponential \
|
||||
backoff with jitter.\n\n## Approach\n\n1. Wrap HTTP calls \
|
||||
in a retry decorator\n2. Use exponential backoff (base 1s, \
|
||||
max 30s)\n3. Add jitter to prevent thundering herd"
|
||||
.into(),
|
||||
state: "opened".into(),
|
||||
author: "alice".into(),
|
||||
assignees: vec!["bob".into(), "carol".into()],
|
||||
labels: vec!["backend".into(), "reliability".into()],
|
||||
milestone: Some("v2.0".into()),
|
||||
due_date: Some("2026-02-01".into()),
|
||||
created_at: 1_736_856_000_000, // ~1 day before frozen
|
||||
updated_at: 1_736_942_000_000,
|
||||
web_url: "https://gitlab.com/infra/platform/-/issues/101".into(),
|
||||
discussion_count: 3,
|
||||
},
|
||||
cross_refs: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_mr_list() -> MrListPage {
|
||||
MrListPage {
|
||||
rows: vec![
|
||||
MrListRow {
|
||||
project_path: "infra/platform".into(),
|
||||
iid: 42,
|
||||
title: "Implement exponential backoff for HTTP client".into(),
|
||||
state: "opened".into(),
|
||||
author: "bob".into(),
|
||||
labels: vec!["backend".into()],
|
||||
updated_at: 1_736_942_000_000,
|
||||
draft: false,
|
||||
target_branch: "main".into(),
|
||||
},
|
||||
MrListRow {
|
||||
project_path: "web/frontend".into(),
|
||||
iid: 88,
|
||||
title: "WIP: Redesign settings page".into(),
|
||||
state: "opened".into(),
|
||||
author: "alice".into(),
|
||||
labels: vec!["ui".into()],
|
||||
updated_at: 1_736_938_400_000,
|
||||
draft: true,
|
||||
target_branch: "main".into(),
|
||||
},
|
||||
],
|
||||
next_cursor: None,
|
||||
total_count: 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture_search_results() -> Vec<SearchResult> {
|
||||
vec![
|
||||
SearchResult {
|
||||
key: EntityKey::issue(1, 101),
|
||||
title: "Add retry logic for transient failures".into(),
|
||||
snippet: "...exponential backoff with jitter for transient network...".into(),
|
||||
score: 0.95,
|
||||
project_path: "infra/platform".into(),
|
||||
},
|
||||
SearchResult {
|
||||
key: EntityKey::mr(1, 42),
|
||||
title: "Implement exponential backoff for HTTP client".into(),
|
||||
snippet: "...wraps reqwest calls in retry decorator with backoff...".into(),
|
||||
score: 0.82,
|
||||
project_path: "infra/platform".into(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// -- Data injection helpers -------------------------------------------------
|
||||
|
||||
fn load_dashboard(app: &mut LoreApp) {
|
||||
let generation = app
|
||||
.supervisor
|
||||
.submit(TaskKey::LoadScreen(Screen::Dashboard))
|
||||
.generation;
|
||||
app.update(Msg::DashboardLoaded {
|
||||
generation,
|
||||
data: Box::new(fixture_dashboard_data()),
|
||||
});
|
||||
}
|
||||
|
||||
fn load_issue_list(app: &mut LoreApp) {
|
||||
app.update(Msg::NavigateTo(Screen::IssueList));
|
||||
let generation = app
|
||||
.supervisor
|
||||
.submit(TaskKey::LoadScreen(Screen::IssueList))
|
||||
.generation;
|
||||
app.update(Msg::IssueListLoaded {
|
||||
generation,
|
||||
page: fixture_issue_list(),
|
||||
});
|
||||
}
|
||||
|
||||
fn load_issue_detail(app: &mut LoreApp) {
|
||||
let key = EntityKey::issue(1, 101);
|
||||
let screen = Screen::IssueDetail(key.clone());
|
||||
app.update(Msg::NavigateTo(screen.clone()));
|
||||
let generation = app
|
||||
.supervisor
|
||||
.submit(TaskKey::LoadScreen(screen))
|
||||
.generation;
|
||||
app.update(Msg::IssueDetailLoaded {
|
||||
generation,
|
||||
key,
|
||||
data: Box::new(fixture_issue_detail()),
|
||||
});
|
||||
}
|
||||
|
||||
fn load_mr_list(app: &mut LoreApp) {
|
||||
app.update(Msg::NavigateTo(Screen::MrList));
|
||||
let generation = app
|
||||
.supervisor
|
||||
.submit(TaskKey::LoadScreen(Screen::MrList))
|
||||
.generation;
|
||||
app.update(Msg::MrListLoaded {
|
||||
generation,
|
||||
page: fixture_mr_list(),
|
||||
});
|
||||
}
|
||||
|
||||
fn load_search_results(app: &mut LoreApp) {
|
||||
app.update(Msg::NavigateTo(Screen::Search));
|
||||
// Set the query text first so the search state has context.
|
||||
app.update(Msg::SearchQueryChanged("retry backoff".into()));
|
||||
let generation = app
|
||||
.supervisor
|
||||
.submit(TaskKey::LoadScreen(Screen::Search))
|
||||
.generation;
|
||||
app.update(Msg::SearchExecuted {
|
||||
generation,
|
||||
results: fixture_search_results(),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Snapshot tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_dashboard_snapshot() {
|
||||
let mut app = test_app();
|
||||
load_dashboard(&mut app);
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("dashboard_default", &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_list_snapshot() {
|
||||
let mut app = test_app();
|
||||
load_dashboard(&mut app); // Load dashboard first for realistic nav.
|
||||
load_issue_list(&mut app);
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("issue_list_default", &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_detail_snapshot() {
|
||||
let mut app = test_app();
|
||||
load_dashboard(&mut app);
|
||||
load_issue_list(&mut app);
|
||||
load_issue_detail(&mut app);
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("issue_detail", &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mr_list_snapshot() {
|
||||
let mut app = test_app();
|
||||
load_dashboard(&mut app);
|
||||
load_mr_list(&mut app);
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("mr_list_default", &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_results_snapshot() {
|
||||
let mut app = test_app();
|
||||
load_dashboard(&mut app);
|
||||
load_search_results(&mut app);
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("search_results", &output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_state_snapshot() {
|
||||
let app = test_app();
|
||||
// No data loaded — Dashboard with initial/empty state.
|
||||
let output = render_app(&app);
|
||||
assert_snapshot("empty_state", &output);
|
||||
}
|
||||
40
crates/lore-tui/tests/snapshots/dashboard_default.snap
Normal file
40
crates/lore-tui/tests/snapshots/dashboard_default.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard
|
||||
Entity Counts Projects Recent Activity
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Issues: 15 open / 42 ● 5m ago infra/platform No recent activity
|
||||
MRs: 7 open / 28 ● 12m ago web/frontend
|
||||
Discussions: 120 ● 8m ago api/backend
|
||||
Notes: 350 (18% system) ● 4m ago tools/scripts
|
||||
Documents: 85
|
||||
Embeddings: 200 Last sync: succeeded
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
40
crates/lore-tui/tests/snapshots/empty_state.snap
Normal file
40
crates/lore-tui/tests/snapshots/empty_state.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard
|
||||
Entity Counts Projects Recent Activity
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Issues: 0 open / 0 No projects synced No recent activity
|
||||
MRs: 0 open / 0
|
||||
Discussions: 0
|
||||
Notes: 0 (0% system)
|
||||
Documents: 0
|
||||
Embeddings: 0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
40
crates/lore-tui/tests/snapshots/issue_detail.snap
Normal file
40
crates/lore-tui/tests/snapshots/issue_detail.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard > Issues > Issue
|
||||
#101 Add retry logic for transient failures
|
||||
opened | alice | backend, reliability | -> bob, carol
|
||||
Milestone: v2.0 | Due: 2026-02-01
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
## Problem
|
||||
|
||||
Transient network failures cause cascading errors in the ingestion pipeline. We need exponential backoff with jitter.
|
||||
|
||||
## Approach
|
||||
|
||||
1. Wrap HTTP calls in a retry decorator
|
||||
2. Use exponential backoff (base 1s, max 30s)
|
||||
3. Add jitter to prevent thundering herd
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Discussions (0)
|
||||
Loading discussions...
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
40
crates/lore-tui/tests/snapshots/issue_list_default.snap
Normal file
40
crates/lore-tui/tests/snapshots/issue_list_default.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard > Issues
|
||||
/ type / to filter
|
||||
IID v Title State Author Labels Project
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
#101 Add retry logic for transient failures opened alice backend, reliability infra/platform
|
||||
#55 Dark mode toggle not persisting across sessi opened bob ui, bug web/frontend
|
||||
#203 Migrate user service to async runtime closed carol backend, refactor api/backend
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Showing 3 of 3 issues
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
40
crates/lore-tui/tests/snapshots/mr_list_default.snap
Normal file
40
crates/lore-tui/tests/snapshots/mr_list_default.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard > Merge Requests
|
||||
/ type / to filter
|
||||
IID v Title State Author Target Labels Project
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
!42 Implement exponential backoff for HT opened bob main backend infra/platform
|
||||
!88 [W WIP: Redesign settings page opened alice main ui web/frontend
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Showing 2 of 2 merge requests
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
40
crates/lore-tui/tests/snapshots/search_results.snap
Normal file
40
crates/lore-tui/tests/snapshots/search_results.snap
Normal file
@@ -0,0 +1,40 @@
|
||||
Dashboard > Search
|
||||
[ FTS ] > Type to search...
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
No search indexes found.
|
||||
Run: lore generate-docs && lore embed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NORMAL q:quit esc:back ?:help C-p:palette o:browser P:scope gh:home gi:issues gm:mrs g/:search gt:timeline
|
||||
Reference in New Issue
Block a user